Rethinking Routing for ReactJS Applications

Traditional routers like the React-Router are good when your application has master-detail style 'navigation'. It fails when your application is composed of components that communicate with each other whose state you want to save in the URL.

Where traditional router works well

A scenario where traditional routers work well is when you have a hierarchy of master-detail style views and you want to navigate across them.

An example is the 'Alerts' UI of DripStat:

Here is how its hierarchy works:

  • You click on the top level 'Alerts' button.
  • It opens a page with a navbar.
  • You click on individual pages like 'Incidents' and 'Violations' to navigate to them

Where traditional router fails

Where traditional routers fail is when you have a bunch of components that depend on each other's state.

Take for example, this typical DripStat dashboard:

It has 7 components with state of their own. The state of each of those components needs to be serialized in the URL so when the user refreshes the page, he sees it in the same state.

The traditional router will suggest putting these states in a hierarchy with a url like this:

/state1/state2/state3/..../state7

It is forcing a hierarchy on our components when there is no absolute hierarchy to speak of. Now every component needs to know the index of the state of every other component in the url, both above and below it. This has many issues:

  1. What if we change the layout of the components on the page, eg, put the Time Range selector below the JVM filter?
  2. What if we introduce an extra component in between?
  3. What if we remove one?
  4. What if we want to reuse a portion of the UI on a different page?
  5. What if we want to load multiple components in parallel, instead of one by one in a hierarchy?

All of the above modifications would require going through and updating the code of every single other component. Each of the component's code would also be full of complex routing code.

The traditional router is simply not designed for this kind of a UI.

Our Solution

Our goal with routing was to:

  1. Keep the router code generic and in a central location. Don't put routing logic in individual components.
  2. Allow changing the layout and number of the components on page, without needing to update routing code
  3. Allow reuse of portions of UI in different pages.
    Eg - 'Pinned Transactions' shows a lot of the same UI as 'Transactions' page.
  4. Components should be able to load in parallel, independently of one another.

We make heavy use of the Flux pattern and Redux to solve this. The state of all the components is kept in a reducer. (Depending on your needs, you may decide to put them all in a single reducer or one reducer per component. We assume single reducer for this article).

All the individual components just subscribe to this reducer to read/write their own state or the state of some other component.

@connect((state, props)=> {
    return {
        uiState: state.uiState
    };
})
class MyDropDown extends React.Component { /*...*/ }

@connect((state, props)=> {
    return {
        uiState: state.uiState
    };
})
class MyRadioButton extends React.Component  { /*...*/ }

We create a single top level component, that serializes/deserializes this reducer to JSON and udpates the URL.

@connect((state, props)=> {
    return {
        uiState: state.uiState
    };
})
class TopLevelPage extends React.Component { 

    //read state from url and initialize
    componentWillMount() {

        const uiState = JSONEncoder.decode(this.props.routeJSON);

        this.props.dispatch(action_initUIState(uiState));
    }

    componentWillReceiveProps(nextProps)
    {
        //if state changed, update url
        if(!isEqual(this.props.uiState, nextProps.uiState))
        {
            this.updateURL(JSONEncoder.encode(nextProps.uiState));
        }
    }
}

We use the React-Router just to route to top level components, and then let our routing mechanism take care of it from there.

Now our components can communicate with each other, can be added/removed and not have to worry about the url. The top level component takes care of that.

We have used this approach throughout DripStat, falling back to React-Router only when it really makes sense, eg on the 'Alerts' page. It has allowed us to rapidly iterate on our UI while keeping it bug-free from routing related issues.

Show Comments