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!

Comparing Marionette and React

The JavaScript landscape is changing rapidly. If you’re anything like me you’ll know that trying to keep current amidst the churn can be frustrating. We can but try, however. To this end I’ve been learning about React and thinking about how it compares to another UI library I’ve used in the past: the Backbone-based library, Marionette.js.

Why React?

  1. React is relatively well-liked. In the 2016 State of JavaScript “Frameworks” survey 3044 respondents claimed to have used React before, 92% of whom claimed they would use it again. React achieved the highest satisfaction rating, Vue finishing a close second with 89%. Vue had only a fraction of React’s users, however: Only 577 respondents claimed to have used it before. Angular 2 (1092/65%), Ember (850/48%) and Angular (3391/47%) lagged significantly behind React and Vue in one or both regards.
  2. Marionette is relatively obscure. In the same survey Marionette was mentioned by just 41 respondents. No data was given for whether these respondents had used it before or would use it again. Meanwhile Backbone, upon which Marionette is based, acquired a relatively low satisfaction rating: Although a relatively high number of users (2077) claimed to have used it before, only about one-third (32%) claimed they would use it again.
  3. React is more library than framework. Unlike full-blown JavaScript frameworks such as Angular, React is mainly a view library. In this sense it it tempting to see React as a more direct replacement for Marionette, much of whose value in my opinion comes from the views it provides. In principle I tend to prefer libraries over frameworks, chiefly because the latter tend to be more prescriptive in terms of how apps should be constructed.

In summary React appears to be a pretty safe choice that should provide a natural progression from Marionette. Let’s put that to the test by creating an app. We’ll implement a simple ticking clock (featured in the React docs) doing it in both Marionette and React so we can compare.

Creating an application

When creating a JS app it’s nice to have a single object that serves as an entry point to the rest of the UI. Both Marionette and React embrace this concept: Marionette via a dedicated Application class; React via a top-level Component that serves as a parent to other, “child” components. So let’s begin implementing our ticking clock by creating a simple application.

Requirements

  • Output “The time according to {library} is:” to the page

Code

Marionette implementation

To implement the requirement in Marionette:

  1. We created a callback function (onStart) for adding a view containing the output to the DOM. This function runs when the application starts. It first creates a region for showing the view, then creates the view, then shows the view in the region.
  2. We created an instance of the Application class.
  3. We attached an event listener (“start”) to the application instance, passing it the callback function defined in #1.
  4. We started the application via the “start” method on the application instance.

React implementation

To implement the requirement in React:

  1. We created a component for adding the output to the DOM. In this case the component is a functional component but as we’ll see later it could also have been a class component.
  2. We added the output to the DOM via ReactDOM.render. This method basically just merges our component with the DOM, similar to Marionette’s region.show method in #1 above.

Analysis

We can see that React makes life a little easier for getting an app off the ground. While Marionette introduces several different concepts right off the bat (Applications, Regions and Views), React introduces just one (Component). React also appears to do things a little more automatically, e.g., no need to officially start the application or create a listener for the start event. React’s syntax also ends up being cleaner and more terse.

Character count

  • Marionette: 296
  • React: 121

Nesting views

Another essential feature of a JS view library is the ability to nest one view within another view. Nested views reflect the treelike structure of the DOM tree and help to encapsulate functionality within clearly defined modules. Both Marionette and React embrace the concept of nested views: Marionette via its dedicated LayoutView class; React via components. So let’s enhance our fledgling app to illustrate how nested views work in each case.

Requirements

  • Add a view for displaying the title
  • Add a view for displaying the clock

Code

Marionette implementation

To implement the requirements in Marionette:

  1. We added two regions to our LayoutView: one for the title, one for the clock. The LayoutView is a special type of view provided by Marionette distinctly for layout purposes.
  2. We created a callback function (onShowLayoutView) for adding views to the regions defined in the layout view. This function runs after the layout view has been added to the DOM.
  3. We attached an event listener (“show”) to the layout view, passing it the callback function defined in #2. Waiting for the parent view to be added to the DOM before attempting to add the child views ensures Marionette will not try to add the latter before the former is ready.

React implementation

To implement the requirements in React:

  1. We created two user-defined components (AppHeader and AppBody) to represent the nested views.
  2. We rendered the nested components in the parent component using the latter’s “render” method.

Analysis

React again seems to make things easier for us. Marionette introduces yet another concept (ItemView); React allows us to reuse an existing one (Component). Marionette makes us do more work (e.g., we have to manually instantiate all new objects); React does this work for us (we can simply define a component declaratively and React will create it for us). React’s syntax again ends up being cleaner and more terse.

CHARACTER COUNT

  • Marionette: 639
  • React: 252

Managing state

The concept of state as it applies to JS apps is admittedly rather new to me: With hindsight I suppose it’s just one of those things I’ve been doing with JS/jQuery/Backbone/Marionette without really thinking too much about how I’m doing it.

To be going on with let’s just define managing state as managing change. In the context of an app the source of change could be manual or automatic. For example:

  • A user enters a value in a text box (manual)
  • A timer updates a value every so often (automatic)

What these cases have in common is that something happens in the app and the app has to respond somehow. In other words, the app must undergo a change in state.

Once again let’s illustrate this by way of our app, this time by implementing the ticking behavior of the clock.

Requirements

  • Store the date so that it can be updated
  • Set a timer to update the date every second
  • Display the date (and redisplay it whenever it’s updated)
  • Clear the timer if the component is ever removed from the page

Code

Marionette implementation

To implement the requirements in Marionette:

  1. We created an ItemView to represent the clock.
  2. We created a Backbone model to store the date, passing the model into the view.
  3. We created a timer (setInterval) for updating the date and set it to update the model’s date attribute every second. We did this within view’s constructor function (initialize).
  4. We created an Underscore template for displaying the date, saving it to the view’s template property.
  5. We instructed Marionette to re-display the view whenever the model’s date attribute changes. For this we used the view’s modelEvents property.
  6. We instructed Marionette to clear the timer if the view is ever removed from the DOM.

React implementation

To implement the requirements in React:

  1. We created a component to represent the clock.
  2. We stored the date in the component’s local state.
  3. We created the timer for updating the date and set it to update the component’s local state every second. We did this in the component’s componentDidMount lifecycle method. This method is called after the component has been rendered to the DOM.
  4. We created a JSX expression for displaying the date, outputting it right there in the component’s render method.
  5. We instructed React to clear the timer if the component is ever removed from the DOM. We did this in the component’s componentWillUnmount lifecycle method.

Analysis

Marionette and React are a little more in line with each other here. There are a similar number of steps involved in implementing this functionality in either framework. The only major difference appears to be related to re-displaying the date. We have to instruct Marionette explicitly to listen for changes to the model so that it knows when to re-display the view. React, on the other hand, propagates changes in state to the component via props.

CHARACTER COUNT

  • Marionette: 976
  • React: 552

Conclusion

Clearly this comparison only touches on the basics of Marionette and React. Equally clearly our ticking clock app is simplistic to say the least. Nonetheless I think the exercise serves to illustrate some interesting differences between the two libraries, differences that for the most part come down in favor of React.

  • I like React’s declarative nature. React seems to do a bit more of the grunt work for us than Marionette. React allows us to declare components; Marionette makes us create objects.
  • I like React’s functional components. Functional code, characterized by pure functions and immutable data, in theory produces more reliable results than object-oriented code. React leans toward the former; Marionette, the latter.
  • I like React’s state management. With React, changes in state are automatically propagated to components via props. With Marionette, again there may be some manual work involved depending on your use case.