Deep Linking with React + Flux

Background

React + Flux

SmartLogic explores Javascript React and Flux So you’ve decided to go with a “Single Page App” architecture. There are lots of javascript libraries (e.g. jQuery, Underscore, Backbone), and frameworks (e.g. Angular, Ember) to choose from, and they all try to make writing client-side apps easier. I’ve used a bunch of these, but lately, I’ve been using React, at least for client-side view code.

  • React is nice because it’s easy to understand and debug, when used as directed

    • one-way data flow
    • re-render everything

However, a full fledged single page app benefits from some additional structure, which React, by itself, is lacking. Flux is a pattern, developed by facebook, intended to bring that structure to the client-side application code that supports the React view code.

Flux is nice because it allows us to separate complicated business logic, which we’re likely to encounter when dealing with larger applications, from our view code, without violating the “rules” of React that make it easy to work with.

Deep Linking

One of the defining features of a Javascript single page app is that it manages browser state changes. That is, when the URL path changes, the client-side code handles everything from showing the appropriate interface, to loading the necessary data.

Where does the router go?

First, a simplified overview of Flux

[Anything] -> Actions -> Stores -> Views

Working from the outside, in:

  • Views (React components) get data from Stores, which they then use to render themselves.

    • Note: since the data is coming from the outside, Views must bind to “change” events from Stores.
  • Each store waits for Actions. When an Action occurs that would affect the state a Store maintains, that Store handles the state change, and emits the “change” event, mentioned above.
  • Actions can originate from anywhere; maybe the app initializes, and loads some initial data, or the user interacts with a View that then kicks off an action.

    • Note: Views never change business logic state directly. By creating actions, the Views are decoupled from the business logic, implemented by Stores.

Read the full documentation here.

So where does the router go?

Perhaps a better question is what does routing solve? At a high level, the router takes a URL, and figures out what the user should be seeing on the page.

Within Flux, the React figures out what the user should be seeing, based on state. That state is determined by Stores, so it follows that a “router”–if we still want to call it that–which turns the state encoded in the URL, into state for our views, would be a part of a Store. Whether that responsibility falls to a single “router” Store, or to many different Stores, depends on the Views which depend on them.

Navigating around the app can be encoded with Actions, to which the Store(s) can respond and update their state, accordingly.

Three Easy Steps to SPA

Step 1. Deep Linking

Suppose you have a To-do list app. Suppose its interface is split into two panes. On the left, your app presents a high-level list of task, and on the right, if a particular task is selected, it shows all the details about that particular task.

Our main React component, which will contain both panes, needs to know which task to show, if any. Following the Flux pattern, we’ll keep track of that in a Store.

So, our Task Store will determine an appropriate view state, based on the URL, when it receives an “initialize app” Action. We’ll trigger this Action on page load, and include with the Action the URL path. We can implement a router with some regex for matching paths, mapped to functions that set Store state as appropriate.

On the backend, we can simply route all non-xhr requests to render the client-side app. This is what that might look like, in Rails.

With those changes, when a user lands anywhere on our site, the backend renders the client-side application, and and client-side app code will handle showing the correct interface, based on the URL.

Step 2. View Changes, without Page Load

However, you may notice that links to view the details for a task still cause the page to reload the entire app. In order to have React re-render the appropriate interface, without a page load, we introduce a new Action: “navigate”.

We replace our links with a component that looks like a regular link, but prevents the page reload, and dispatches a “navigate” Action, instead.

The Store is modified to also run its routing logic against “navigate” Actions, just like on the “initialize app” Action.

Now, our app re-renders via React, rather than fetching the entire page from the server.

You’ll notice that navigating around our app no longer updates the URL path. To get this back, we create a special “URL Store”, whose responsibility is to maintain the state of the URL. All it does is listen for “navigate” Actions, and use window.history.pushState to update the URL.

Now, clicking links to view particular tasks

Step 3: A Working “Back” Button

Add a window.onpopstate listener somewhere in your app (I recommend the URLStore), which dispatches a “navigate” action. The onpopstate event handler receives whatever state was included as an argument window.history.pushState, so we need to update the URLStore to pass in path of previous “navigate” actions.

So that clicking “Back” doesn’t add the previous page to our browsing history, we differentiate between “navigate” Actions that should update the history, from those that should not, with a “pushState” flag, on the Action. The event handler called when the user navigates “Back” dispatches Actions that do not update pushState. The URL Store now looks like this.

We update the AppLink component, so that it will update browser history.

Since our TaskStore already handles the state changes necessary for “navigate” events, and our TasksApp React component knows how to turn TaskStore state into a UI, that’s all we have to do, for our app to work as expected, when navigating back through history.

Want to learn more? Follow SmartLogic on Twitter.

comments powered by Disqus