With the help of Create React App, I made a web app that displays a timeline of events. Each event is associated with one or more topics, and users can filter the timeline by topic with the help of a small form at the top of the page. (For example: a user loads the page and sees a timeline of events; the user interacts with the form to search for a specific topic; the user submits the topic through the form; the timeline gets updated accordingly.)
Note: Sorry in advance if I will be using ES6 classes instead of just functions. I will reconsider my writing style in the coming weeks :)
Problem setting
At the core of my web app is an App component, which renders two sub-components:
FilterForm, a form with a search bar and a submit button;Timeline, a list of items related to the last topic that was submitted throughFilterForm.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [],
topic: ''
};
this.updateTopic = this.updateTopic.bind(this);
this.updateTimeline = this.updateTimeline.bind(this);
}
componentDidMount() {
this.updateTimeline();
}
updateTopic(event) {
this.setState({topic: event.target.value});
}
updateTimeline(event) {
// avoid refreshing the page:
if (arguments.length > 0) {
event.preventDefault();
}
// update the state of the timeline:
fetch('/api?topic=' + this.state.topic)
.then(res => res.json())
.then(items => this.setState({items: items}));
}
render() {
return <React.Fragment>
<FilterForm
topic={this.state.topic}
updateTopic={this.updateTopic}
updateTimeline={this.updateTimeline}
/>
<Timeline items={this.state.items} />
</React.Fragment>;
}
}
FilterForm is a controlled component that calls the updateTopic method of App:
class FilterForm extends React.Component {
constructor(props) {
super(props);
this.updateTopic = this.props.updateTopic;
this.updateTimeline = this.props.updateTimeline;
fetch('/api/tags')
.then(res => res.json())
.then(tags => tags.map(tag => <option value={tag} key={tag} />))
.then(options => this.options = options);
}
render() {
return <form onSubmit={this.updateTimeline}>
<label id="topic">Topic: <input
type="text"
value={this.props.topic}
onChange={this.updateTopic}
list="topics"
/>
</label>
<datalist id="topics">
{this.options}
</datalist>
<label>
<input type="submit" value="Update" />
</label>
</form>;
}
}
Timeline is really just a list of items:
class Timeline extends React.Component {
render() {
return <ul className="Timeline">
{this.props.items
.map((item, idx) =>
<TimelineItem
key={item.name}
name={item.name}
topics={item.topics}
/>
)
}
</ul>;
}
}
And finally a TimelineItem is a list item that contains a sub-list with all the topics to which this item is related:
class TimelineItem extends React.Component {
render() {
return <li className="TimelineItem">
{this.props.name}
<ul className="tagsList">
{this.props.topics.map(topic =>
<li key={topic}>{topic}</li>)}
</ul>
</li>;
}
}
Problem
Given this setup, I decided that each topic choice should be associated with a unique URL like /?topic=example: opening this URL in a browser should show a timeline about the topic example, and vice-versa submitting a topic through the HTML form would update the URL accordingly. In order to add this features, I thought I'd use React Router.
I am new to React Router, so after some reading I thought I'd do the following:
- wrap my
Appcomponent inside aBrowserRouterelement export default withRouter(App), instead of justexport default App- in the
Appconstructor, get thetopicquery parameter from the URL (new URLSearchParams(this.props.location.search).get('topic')) and use it to initializethis.state.
This way, manually setting the URL works as expected: the timeline shown depends on the URL, and the HTML form contains the topic set in the URL. However, the reverse doesn't hold: when submitting the HTML form, the URL doesn't update.
Additional problem
Moreover, I also wanted to have each item show a sub-list of links instead of just topics: clicking on a topic inside an item would update the timeline to match the topic that was clicked. Now, substituting {topic} with <Link to={'/?topic=' + topic}>{topic}</Link> inside TimelineItem updates the URL, but doesn't update the timeline itself.
Questions
I'd like some general input on my solution to the two problems I have described, and to do this you could answer the following specific questions:
- Is my web app ready to get these two new features, or should it be radically changed?
- Is it OK to not have any
actionattribute inside my HTML form and just rely on JavaScript, like I am doing now? - How can I have the URL reflect what was submitted through the form? (problem 1)
- What's missing to have my React Router
Links actually refresh the page? (problem 2)
Thank you in advance, and please let me know if you need even more information.
P.S. please note that /api/tags, /api?topic=example and /?topic=example are three different endpoints: the first two talk to the back-end, while the third one is the front-end.