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.

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:

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:

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:

  1. You type something in the input box (or hit backspace)
  2. React calls the SearchBox’s handleChange method (with an event)
  3. SearchBox calls the passed onTextChange callback (with a string). This corresponds to the FilterableNoteList’s setSearchText method
  4. The FilterableNoteList trims and lowercases (for a case-insensitive search) its argument, then sets searchText in its own state
  5. 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
  6. 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.

You can follow me on Mastodon or this blog via RSS.

Creative Commons BY-NC-SA
Original text and images (not attributed to others) on this page are licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.