Anatomy of a React application: routing

Anatomy of a React application: routing

Anatomy of a React application: routing

In this chapter of the Anatomy of a React application series I'll be covering routing.

Routing is the part that drives which page and component should displayed based on the requested URL or the application state. This can be implemented in number of ways varying from custom code within your application to the use of one of the few React routing libraries.

Ideally routing should be defined as a declarative, first-class component of your web application in order to leverage the benefits of the React approach with navigation. It is also important for the routing implementation to be working both server and client side so that the correct view can be immediately loaded on the first request and navigation history can be maintained correctly on your browser.

The approach I first experimented with is a combination of react-router and react-router-redux.
This leverages the declarative routing provided by react-router with the application state management of redux so that the navigation history can be kept in sync with state.

The various parts of this implementation are:

  • On the initial server side rendering, a memory history object is created and used to initialise the React state using createMemoryHistory.
    const memoryHistory = createMemoryHistory(req.url);
    const store = configureStore(memoryHistory, preloadedState);
    const history = syncHistoryWithStore(memoryHistory, store);
    view on github.com
  • The entry point file on the client also creates a history object using browserHistory. The difference between these two ways of initialising history is that createMemoryHistory does not read or manipulate from the address bar and therefor is used on the initial server-side rendering. (Note that this is different in the latest version of react-router as react-router-redux does not currently support the latest react-router as explained later).
    import React from 'react';
    import React from 'react';
    import { render } from 'react-dom';
    
    import { Provider } from 'react-redux';
    import { Router, browserHistory } from 'react-router';
    import { syncHistoryWithStore } from 'react-router-redux';
    
    import configureStore from '../common/store/configureStore';
    import routes from '../common/routes';
    
    const store = configureStore(browserHistory, window.__initialState__);
    const history = syncHistoryWithStore(browserHistory, store);
    
    render(
      <Provider store={store}>
        <Router history={history} routes={routes} />
      </Provider>,
      document.getElementById('app')
    );
    view on github.com
  • Redux router is added to the middleware and is responsible for transitioning between views via redux state changes.
    const configureStore = (history, initialState) => {
      const store = createStore(
        reducer,
        initialState,
        compose(
          applyMiddleware(
            routerMiddleware(history),
            thunk,
            createLogger,
            actionMiddleware,
          ),
        )
      );
    view on github.com
  • The route file from the common sources defines the main component (src/common/components/App), the index route (typically a home component mapped to /) and paths-to-components relationships. A catch-all option will then route all paths not listed here to a not found page.
    <Route path="/" component={App}>
       <IndexRoute component={Home} />
       <Route path="about" component={About} />
       <Route path="timer" component={Timer} />
     </Route>
    view on github.com
  • The App component is defined as a generic wrapper including common layout parts of the application (e.g. header, footer, navigation). Its children will be passed to the App component based on the matching of the route configuration.
    const App = ({ children }) => (
      <div>
        <!-- Header... -->
        <!-- Links... -->
        {children}
        <!-- Footer... -->
      </div>
    );
    
    App.propTypes = {
      children: PropTypes.shape().isRequired,
    };
    
    export default App;
    view on github.com
  • Links are defined as following:
    <Link className="nav-link" to="/somePath">SomeComponent</Link>

By using this approach, routing and navigation is built as a core, declarative part of your application with only few lines of code thanks to react-router and react-redux-router.

The one major downside I found when playing with this approach is the fact that, as mentioned earlier, react-router-redux does not support the latest version of react-router and therefor the latter cannot be upgraded (the development of react-router-redux compatible with react-router@4.x.x is currently in beta). This is an issue often affecting using third-party libraries: if the library itself not up-to-date, buggy, not actively developed, or implements architectural decisions that don’t fit well with your application, you will be restricted in its usage.