Note Taker assumes form
May 13, 2014
Part 2 in a series.
Today I made lots of progress on my Note Taker mini-project:
Everything in the screenshot is functional.
- We’re looking at notes in the “Main” folder, reflected in the URL, as well as in the folder list on the left, where “Main” is bolded.
- Clicking on “Archive” or “Trash” lists notes in those folders.
- We’re filtering on “mor”, which appears in the two notes displayed — “more things I bought” and “morning”.
- You can click on a note to view it in full, edit it (it saves automatically), archive or delete it.
- Lastly, the Pure-based design is totally responsive.
Props versus state
The distinction between React’s props and state has finally started to crystallize in my mind. Props are data passed down to a component by its parent, and they can’t be changed. State is managed by the component itself, and it can change its own state.
A common use case is for a parent component to manage some data as state, and selectively pass parts of that data down to each of its children as props.
In Note Taker, the state I have to deal with is:
- The notes. (Each has a string of contents, a creation time, a unique ID, and a folder name that it’s in, i.e., “main”, “archive”, or “trash”.)
- Whether you are currently viewing a folder or an individual note.
- If you’re viewing an individual note, its ID.
- If you’re viewing a folder, its name, such as “main”.
- If you’re viewing a folder, the search text (“mor” in above screenshot).
Most of this lives in the global App component as state, and is passed down to child components as props. For instance, the FolderList needs to know which folder we’re viewing, so it can bold that folder’s name. So App passes FolderList the folder currently being viewed as a prop.
The one piece of state that did not make sense to place in the global App component was the search text. If you recall the relevant piece of the hierarchy from Day 1’s report:
- FilterableNoteList: lists notes in a folder
- SearchBox: lets you type search terms to filter notes
- NoteSummaryList: lists notes in folder which match search term
A FilterableNoteList includes a box to type a search term, and ought to be able to encapsulate the notion of filtering, so at a higher level I don’t have to worry about it.
To do that, I make searchText
part of the FilterableNoteList’s
state, and pass it down to the SearchBox as props, along with a
callback to change the search text:
var FilterableNoteList = React.createClass({
getInitialState: function() {
return {searchText: ''};
},
setSearchText: function(newText) {
this.setState({searchText: newText.trim().toLowerCase()});
},
render: function() {
var searchText = this.state.searchText;
return /*..*/(
SearchBox({
searchText: this.state.searchText,
onTextChange: this.setSearchText
})
/*..*/);
}
}
Then the SearchBox uses its search text prop as the default value of
an <input>
element, with a handler that calls
this.props.onTextChange
:
var SearchBox = React.createClass({
handleChange: function(event) {
this.props.onTextChange(event.target.value);
},
render: function() {
return React.DOM.form({className: 'pure-form'},
React.DOM.input({
className: 'search-box',
defaultValue: this.props.searchText,
onChange: this.handleChange
}));
}
});
So what exactly happens here is:
- You type something in the input box (or hit backspace)
- React calls the SearchBox’s handleChange method (with an event)
- SearchBox calls the passed onTextChange callback (with a string). This corresponds to the FilterableNoteList’s setSearchText method
- The FilterableNoteList trims and lowercases (for a case-insensitive search) its argument, then sets searchText in its own state
- This change triggers a rerender of the FilterableNoteList,
instantiating a new SearchBox with the new searchText as a prop.
But note that because of React’s DOM
diffing, the actual
<input>
element in the DOM isn’t destroyed and recreated - The FilterableNoteList rerender also rerenders the NoteSummaryList inside, so an updated set of notes is displayed, reflecting the new search string
Whew. It seems like a lot written out like that, but conceptually it’s simple. As a user of React, I wouldn’t normally think about all those steps explicitly — I just figure out where the state should live and specify my data dependencies, and the rest falls out naturally.
Routing
Since React doesn’t include a routing library, I’ve opted for
Director, which is pleasantly
easy and common-sense. It works basically the same as Backbone’s
Router, from what I remember. I call the router’s setRoute
method to
navigate to a folder or note, and each route sets top-level state in
the App component:
var mountedApp = React.renderComponent(App(), reactEl);
var router = Router({
'/(main|archive|trash|)': function(folder) {
mountedApp.setState({viewType: 'list', folder: folder || 'main'});
},
'/note/:id': function(id) {
mountedApp.setState({viewType: 'note', noteId: id});
}
});
Using the return value of React.renderComponent here is very important. Initially I did it wrong:
var app = App();
React.renderComponent(app, reactEl);
// ... later ...
app.setState(/* ... */);
That doesn’t work because the App instance returned from the App()
constructor is not necessarily the instance that gets mounted in the
DOM.
How I found that out is a great example of why I love React. With most JavaScript frameworks, my mistaken code would simply fail silently, but React gave me an invariant violation message in the console with a link to this detailed explanation. React has a bunch of helpful warnings like this and they get compiled away to nothing in the optimized, production builds. All libraries should be so generous.