Thinking in Navigation

Thinking in React is a featured post on Facebook's React website. Step by step, it takes you through the thought process of building a searchable product data table in React. It's a great post but the thought process is flawed. I'll explain the flaw and how you can use the Navigation router to correct it.

The flaw appears in step 3 when the author identifies what data belongs in React state. To make the identification he asks three questions of each piece of data:

  1. Is it passed in from a parent via props? If so, it probably isn't state.
  2. Does it change over time? If not, it probably isn't state.
  3. Can you compute it based on any other state or props in your component? If so, it's not state.

From these questions he concludes that the search text and the checkbox make up the React state. It might seem like a harmless conclusion but the problem is that there's one question missing. The author forgot to ask:

  1. Is it part of the URL? If so, it's not state.

The addition of that one question results in quite a different conclusion to step 3:

The search text the user has entered and the value of the checkbox should appear in the URL. Otherwise the filtered product data table isn't bookmarkable and can't be linked to. Data that's part of the URL lives with the router, not in React state.

The author, in the subsequent two steps, identifies which component owns the data and inversely flows the data up the hierarchy to this common owner. But these steps no longer make sense now we know that the data's owned by the Navigation router and not a React component. I'll rewrite steps 4 and 5 in light of the new conclusion to step 3.

Step 4: Add Navigation to the one-way data flow

So far, we've built an app that renders correctly as a function of props flowing down the hierarchy. Now it's time to add the Navigation router into this one-way data flow.

If you try to type or check the box in the current version of the example, you'll see that React ignores your input. This is intentional, as we've set the value prop of the input to always be equal to the prop passed in from FilterableProductTable.

Let's think about what we want to happen. We want to make sure that whenever the user changes the form, we update the props to reflect the user input. Since components shouldn't update their own props, we need to re-render FilterableProductTable passing in the new form values. We can use the onChange event on the inputs to pass the form values into the Navigation router whenever either one changes. Navigation will update the URL which triggers a call to our renderView function. We can return the FilterableProductTable with the new prop values, and the app will be updated.

Though this sounds complex, it's really just a few lines of code. And it keeps the data flowing one way down the component hierarchy.

Step 5: Clean up the browser history

Ok, we've made the filtered product table bookmarkable. The problem is that now every time the user updates the search text box the URL also updates: type in "ball" and you have to press the browser back button four times to clear the text.

We still want to filter the product data table as the user types but we don't want to add a history record until there's a pause in the typing.

To stop the Navigation router creating a history record we can pass it a historyAction of "none" along with the form data. Then in the renderView function, we can create a timeout to delay the addition of the history record. By clearing the previous timeout at the top of the renderView function, a history record is only created if there's a long enough pause in the user activity.

The finished app is very similar to the static version of the app from step 2. The components still only have render methods and the data still only flows in one direction. The difference is that now it's bookmarkable and works with the browser back button. :)