Getting Rowdy With React Router

React Router Announces V4 Rewrite

React Router has pre-released a complete rewrite to their library tagged under v4. It highlights a React style coding approach that is mostly declarative rather than imperative. Being a complete rewrite, version 2.x and 3.x will only have minor revisions and v4.x will be the focus. This is good for us! I get it; it's a pain to change your entire application's routing configuration. However, I think it's a smart move to upgrade. No longer do you need to asynchronously load routes, something will always render (your choice). You also don't need a top level routing configuration! You can route from anywhere in your components if you desire. I believe the decision to build alongside React in a composable and declarative way will only make React Router a staple for SPA's. This article is a part of a series introducing the improved React Router, Starting an Application with React Router, and Adding Authentication to your SPA.

React Router v4.x Highlights

Below are a few examples of what has changed in this major release. This is based off React Router's FAQ, and I'm simply giving context to what v2.x and v3.x could look like next to v4.x.

  • Declarative and Composable Routing
  • Removal of Lifecycle Events
  • Using a Routing Config
  • Noteable API Changes

Declarative and Composable Routing

One of the first statements the React-Router team mentioned was the overall improvement to their approach of re-writing this library using declarative and composable coding methodologies. Quickly they realized they were fighting against React with their previous versions.

v2.x had a couple ways to configure your routing components, but this configuration was static and routes couldn't be defined outside of the configuration. As stated in their Route Configuration Docs, they created a set of instructions that tell a router how to try and match the URL. Looking at Declarative Programming, we would notice that creating a set of instructions disqualifies this code from being declarative. Lets take a look at the code below and compare the differences.

render((
  <Router>
    <Route path="/" component={App}>
      <Route path="company" component={Company} />
      <Route path="messages" component={Messages}>
        <Route path="messages/:userId" component={Message} />
      </Route>
    </Route>
  </Router>
), document.body);

At first glance, that might not seem bad. But there are some gotcha's with this configuration: this is assuming we need to tell the Router every single Route that we want in this application. Sure, you could have separate routing files relative to your component directories, but you still have to include them in your root routing configuration.

Alternatively, v4.x does use a parent component of Router but we're free to define routes throughout our components! Here's an example where we have a root component and child components that match other routes.

import React from 'react';
import Router from 'react-router/BrowserRouter';
import Match from 'react-router/Match';
import Link from 'react-router/Link';

// Let's call this our 'root' component
// that renders a few top level components (Dashboard and Messages)
const Example = () => (
  <Router>
    <div>
      <ul>
        <li>Dashboard</li>
        <li>Messages</li>
      </ul>

      <Match pattern="/" component={Dashboard} />
      <Match pattern="/messages" component={Messages} />
    </div>
  </Router>
);

// This is a component used in our root Example component
// that contains more routes to use! We are now able to
// visit messages from a user. The `pathname` property is passed in
// from the `Match` component used in the Example component.
// This is a part of the react-router API.
const Messages = ({ pathname }) => (
  <div>
    <h1>Messages Component</h1>

    <ul>
      <li>
        <Link to={`${pathname}/123`}>Messages from userId 123</Link>
      </li>
    </ul>

    <Match pattern={`${pathname}/:userId`} component={Message} />

    <Match
      pattern={pathname}
      render={() => (
        <h1>Default Component Rendered in Messages</h1>
      )}
      exactly
    />
  </div>
);

// Message has a property object named `params`
// that is made available by parsing the `pathname`.
const Message = ({ params }) => (
  <div>
    <h1>{params.userId}</h1>
  </div>
);

const Dashboard = () => (
  <div>
    <h1>Dashboard Component</h1>
  </div>
);

No longer do you have to rely on a root route configuration as a hierarchy of routes. The components themselves define the hierarchy. I think we'll see more of this type of integration with React as React Router moves forward. This is a fantastic addition to the library and really shows what a declarative thought process looks like.

Removal of Lifecycle Events

React Router had a handful of lifecycle methods such as onEnter, onLeave, and onChange. These are going away in v4.x. Their reasoning was perfect: React already handles all of this for you. Why would you want to wrap those methods to provide a new API to your consumers? In my opinion, this is what I like to see. They are leveraging the tools already provided to us and making our lives a lot easier.

An example of using these lifecycle hooks looked like this in v2.x

<Route path="/messages/:userId" onEnter={someFunctionHere} />

Now, we don't have to worry about onEnter, onLeave, and onChange. We can handle this natively in React itself using componentDidMount, componentWillUnmount, and componentWillReceiveProps. You can review all of React's component lifecycle methods here, and I imagine they work completely fine with the new and improved React Router.

Here's an implementation in v4.x that behaves the same way as onEnter would work. App contains a Link and a Match component that will end up rendering our ExampleComponent. Instead of having an onEnter on our Match, we use componentWillMount on the component we're trying to render, and it achieves the same result.

import React, { Component, PropTypes } from 'react';
import Link from 'react-router/Link';
import Match from 'react-router/Match';

export const App = () => (
  <div>
    <Link to="/messages/123">User</Link>

    <Match
      pattern="/messages/:userId"
      component={ExampleComponent}
      exactly
    />
  </div>
);

export class ExampleComponent extends Component {
  static propTypes = {
    someFunctionHere: PropTypes.func.isRequired,
  };

  componentWillMount() {
    const { someFunctionHere } = this.props;

    someFunctionHere();
  }

  render() {
    return (
      <p>Heres a component with an onEnter representation</p>
    );
  }
}

Using a Routing Config

Previously, we touched on static route configurations and how v4.x allows you to move away from that if you desire. However, for those that do find value in a root route configuration, you will still be able to define your routes in one place.

In v2.x we relied upon a root configuration that would pull in our routes and we could then define components that the route would render. This is a very basic example and would turn into a behemoth with code splitting involved.

import Dashboard from 'components/Dashboard';
import Messages from 'components/Messages';
import Message from 'components/Message';

import {
  someMoreExportedRoutes,
  evenMoreExportedRoutes,
} from 'root/routes'

export default {
  childRoutes: [
    {
      path: '/',
      getComponent: Dashboard,
      childRoutes: [
        onEnter: doSomethingHere,
        {
          path: '/messages',
          getComponent: Messages,
          childRoutes: [
            {
              path: '/:userId',
              getComponent: Message,
            },
          ],
        },
      ],
    },

    someMoreExportedRoutes,
    evenMoreExportedRoutes,
  ],
};

Above we have a route configuration that defines a root route of Dashboard with an additional onEnter hook for the child routes it may contain. The / route also has child routes containing a place for messages and a single message. As you can see, we could infinitely nest childRoutes, and it becomes a little tiring to read, especially if we're adding lifecycle events to routes.

Taking a look at that, feast your eyes on the new hotness in v4.x

import Dashboard from 'components/Dashboard';
import Messages from 'components/Messages';
import Message from 'components/Message';

import {
  someMoreExportedRoutes,
  evenMoreExportedRoutes,
} from 'root/routes'

const routes = [
  {
    pattern: '/',
    component: Dashboard,
  },

  {
    pattern: '/messages',
    component: Messages,
    routes: [
      {
        pattern: '/:userId',
        component: Message,
      },
    ],
  },

  someMoreExportedRoutes,
  evenMoreExportedRoutes,
];

Both configurations are the same. Notice the structure and how much easier it is to follow in v4.x. Because we can't and shouldn't configure lifecycle methods in our root route configuration, it immediately becomes a simple import of straight forward routes.

If you do go with a route configuration file in v4.x you'll need to wrap the Match component and use that as a replacement. You can view the full example on their docs.

const MatchWithSubRoutes = route => (
  <Match {...route} render={props => (
    <route.component {...props} routes={route.routes} />
  )} />
);

Notable API Changes

There is a brand new API made available by v4.x, not surprising when the entire package was re-written. I think it would be safe to say that all or most of v2.x has vanished and has been carefully replaced with declarative and thoughtful methods. I don't see a definitive list of what has been cut and what has been added, so I'll do my best and touch on a few high level changes.

  • <Router>: Replaced by <BrowserRouter>, <HashRouter>, <MemoryRouter>, or <ServerRouter>. Each Router provides a separate use case and can be chosen by your application's standards.
  • <Link>: Now in a more declarative and accessible format. It functions the same, providing navigation around the application. A few properties have changed though: activeOnlyWhenExact, isActive, location, and children have been added. onlyActiveOnIndex and onClick have been removed or moved.
  • <Match>: This is a new way to render UI by pattern matching a location. I would call this the "bread and butter" of React Router.
  • <Miss>: A new way to render something if a location does not match a <Match> route. If we couldn't find a user for instance, we could put a <Miss> component into play and it would render instead.
  • <NavigationPrompt>: An addition that prevents a user from navigating away from a page. For example, if a user was filling out a form, we may want to render a <NavigationPrompt> to make sure they want to leave without destroying progress.
  • <Redirect>: This functions mostly the same. I don't think the new API will have multiple components like <IndexRedirect>. You are free to use a router to handle redirects instead.

Conclusion

Overall, there are a lot of changes that have made it into v4.x and far too many to cover in a single article. This version is still in a pre-release state, and even more changes could be coming down the line. What we've covered here should be enough to understand the path React Router is taking and ease you in to the migration from previous versions. Personally, I will be upgrading my projects as soon as the release hits. Please check out the React-Router Documentation and try the examples! Next, we will be Starting an Application with React Router!

Sources