URL routing with React

In my last post I demonstrated a possible approach to hash-based URL routing with Marionette. In this post I’ll demonstrate a possible approach to the same issue with React. To do this I’ll recreate the tabbed UI I developed last time, this time using React and React Router.

React Router

React Router is a routing library built on top of React. It allows a developer to implement URL routing in a React app using a variety of components. For my demo I’ll be using the following components:

Demo app with React

The code for the app is available on CodePen. Before diving into the implementation details I’ll first provide a brief overview of the requirements, data model and code design.

Requirements

The requirements for the React version of the app will be the same as those for the Marionette version.

Data model

The data model for the React version of the app will be similar to that of the Marionette version. The only difference is that the model will no longer require an active attribute. In the Marionette version this attribute was used by the tabs and tab content views to determine the visible state of a tab or tab panel. In the React version this job will be performed by comparing a parameter contained in the route to the id attribute of the model (see Creating the tabs and Creating the tab content).

Code design

In a previous post I demonstrated how React apps can be composed from a series of nested components. For example my ticking clock app was composed from two sub-components (Clock and AppHeader) nested within a top-level component (App). The top-level component was then rendered to the DOM with the ReactDOM render method. This app will be composed in a similar fashion and will consist of the following sub-components:

  • AppRouter
  • React stateless functional component
    Provides URL routing. Wraps ReactRouter.Router.
  • App
    React stateless functional component
    Serves as a container for the rest of the application code.
  • Tabs
    React stateless functional component
    Represents a collection of tabs.
  • TabContent
    React stateless functional component
    Represents a collection of tab panels.

It will also leverage a similar version of the loadInitialData function I created last time for loading tab content into the UI.

Setting up

Our app has a few dependencies: React, ReactDOM, React Router and jQuery. In the CodePen I add these dependencies from a CDN via the JavaScript Pen Settings. As a convenience, I also save shortcuts to the specific React Router components using destructuring assignment:

const {Router, Route, Link, hashHistory} = ReactRouter;

Loading the data

const loadInitialData = () => {
  const dfd = $.Deferred();
  dfd.resolve([
    {id: 1, title: 'Tab one', description: 'This is the first tab.'},
    {id: 2, title: 'Tab two', description: 'This is the second tab.'},
    {id: 3, title: 'Tab three', description: 'This is the third tab.'}
  ]);
  return dfd.promise();
};

The data for the app is loaded in a very similar fashion to last time: To mimic loading the data from a server, a function simply returns a promise of the data. This time the data is exposed as a plain JavaScript array instead of a Backbone collection. The Backbone collection worked nicely with the Backbone-based Marionette views I used last time; the JS array will work nicely with the stateless functional components I’ll be using for the React version’s views.

Creating the router

const AppRouter = (props) => {
  return (
    <Router history={hashHistory}>
      <Route path="/" component={App} {...props}>
        <Route path="/tabs/:tab" component={App} />
      </Route>
    </Router>
  );
};

To create the router I wrap a Router component in a stateless functional component (AppRouter). A stateless functional component is simply a function that takes the component’s props as an argument. I then use the Router’s history prop to instruct React Router to keep track of application state using hashHistory. Finally I use two Route configuration components to define the routes into the app:

  1. The first route matches the top-level path (/) and is associated with the yet-to-be-defined App component. Associating a route with a component basically just means that the component will be rendered whenever the route’s path is matched. This route also receives any props passed into AppRouter. Using spread syntax passes in any and all props.
  2. The second route matches a nested path (/tabs/:tab). This route is also associated with the App component.

Creating the app

const App = ({route, params}) => {
  const activeTab = Number(params.tab) || route.defaultTab;
  return (
    <div>
      <div><h1>URL routing with React</h1></div>
      <Tabs tabsCollection={route.tabsCollection} activeTab={activeTab} />
      <TabContent tabsCollection={route.tabsCollection} activeTab={activeTab} />
    </div>
  );
};

To create the App component I once again use a stateless functional component. Since App is associated with a route it receives route and params as props. The route prop corresponds to the route object that is rendering the component; the params prop corresponds to any URL params included in the route’s path.

App’s first responsibility is to identify the active tab. This information comes either from the URL params or, if no corresponding param is present, from the route object. It then returns the component’s element, which in this case comprises a simple heading and the sub-components Tabs and TabContent. Both sub-components take tabsCollection and activeTab as props.

Creating the tabs

const Tabs = ({tabsCollection, activeTab}) => {
  return (
    <ul>
      {tabsCollection.map(({id, title, description}) => {
        let tab;
        if (id === activeTab) {
          tab = <span>{title}</span>;
        } else {
          tab = <Link to={`/tabs/${id}`}>{title}</Link>;
        }
        return <li key={`tab-${id}`}>{tab}</li>
      })}
    </ul>
  );
};

A stateless functional component can also be used to create the Tabs component. The component returns an unordered list representing the collection of tabs with each list item corresponding to an individual tab. To determine whether an individual tab should be presented in an active or inactive state, the component uses conditional rendering . It does this by iterating over tabsCollection and comparing the tab model’s id attribute with the value of activeTab. If these values are the same, the tab will be rendered without a hyperlink so that it can’t be clicked; if they’re not the same, the tab will be rendered with a hyperlink so it can be clicked.

To render the hyperlink itself the component uses a Link component, a “location-aware” component used for navigation. In this case the value of the hyperlink’s href attribute is passed into the Link using the latter’s to prop. Notice how the prop’s value, /tabs/${id}, matches the nested path, /tabs/:tab, on the Router.

Finally the Tabs component returns the list item itself, adding a key prop to uniquely identify the element among its siblings.

Creating the tab content

const TabContent = ({tabsCollection, activeTab}) => {
  return (
    <div>
      {tabsCollection.map(({id, title, description}) => {
        if (id === activeTab) {
          return (
            <div key={`tab-panel-${id}`}>
              <h2>{title}</h2>
              <p>{description}</p>
            </div>
          );
        }
      })}
    </div>
  );
};

Almost the same process that was used to create the Tabs component can be used to create the TabContent component. Once again a stateless functional component returns a root element (in this case a DIV) that represents a collection of items (in this case tab panels). Inside the root element a JSX expression determines whether an individual item should be rendered in an active or inactive state. Again this is done by comparing the value of the tab model’s id attribute with the value of the activeTab prop. If these values are the same the item is shown; if they’re different the item is hidden. Once again a key prop is added to each item to uniquely identify the element among its siblings.

Acceptance testing

To test that the app meets the requirements, export the CodePen to a ZIP, unzip the archive and open index.html in a browser.

Conclusion

React, in conjunction with React Router, ostensibly makes it easier to implement hash-based URL routing than Marionette. The general approach facilitated by React of declaring and composing components–especially stateless functional components–cuts down on some of the procedural cruft around the instantiation of objects necessitated by Marionette. As such I find myself liking React’s approach, if for no other reason than it appears to require less code. In my experience, the less code, the better!

Leave a Reply

Your email address will not be published. Required fields are marked *