Updating to React Router v4

Posted by Niky Morgan on November 28, 2017

Our team recently updated Learn.co from React 15 to 16, and I took part in a subsequent sprint to update our React Router from version 2 to version 4. React Router 4 is conceptually very different from the previous versions; the creators wrote the new version entirely in React with an API that followed common React patterns. The Route is now just a component which makes nesting routes an easier task. This design difference meant we had to implement React Router 4 in a completely different manner than we used 2. Here are some of the biggest changes we made in our upgrade.

Package Name

React Router 4 is broken into two packages react-router-dom is for webpages and react-router-native for React Native apps. To use version 4, install one of these options instead of react-router.

Decentralized Routing

In previous versions of React Router, the routes were centralized in one top-level file. Nested routes were created by wrapping child routes within a parent route component. The parent component then rendered the components it wrapped as this.props.children.

const HomePage =() => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>
const Navbar = (props) => <div>Navbar {props.children}</div>
const UserPage = () => <div>One User</div>

const App = () => (
  <Router history={browserHistory}>
    <Route path='/' component={Navbar} >
      <Route path='/home' component={HomePage} />
      <Route path='/users' component={UsersPage} />
      <Route path='/users/:id' component={UserPage} />
    </Route>
  </Router>
)

In the version 2 example above, the / parent route renders a Navbar which should be visible on all routes. In order for that to be the case, the / route must wrap all the other routes and the component it renders must render the nested routes as its children. In version 2 this nested routing was the only way components could share content unless the same code was copy pasted (or imported) in each of them. For example, if /users needed to share a sidebar with /users/:id, the /users route would have to wrap /users/:id. This leads us to the next big change in React Router 4.

Inclusive Routing

In React Router 4 routes are now inclusive. Previous versions of React Router would only render the first route that exactly matched a path (unless nested). Router v4 renders routes inclusively, a single path can match multiple unnested routes which each render a component.

In the previous v2 example, at the path users/1 the router renders only the User component because it is looking for an exact match.

In contrast, the below v4 example renders inclusively and accepts partial matches. At the path users/1 the router would match and render the Navbar, Users and User components because those paths are all included in the string /users/1. No need to wrap the / route around the other routes anymore!

(Note: The extra div wrapping the routes is because BrowserRouter only accepts a single child element. A better solution would be to render the routes in a component which is wrapped in BrowserRouter, but I wanted to keep this similar to the previous example.)

const App = () => (
  <BrowserRouter >
    <div>
      <Route path='/' component={Navbar} />
      <Route path='/home' component={Home} />
      <Route path='/users' component={Users} />
      <Route path='/users/:id' component={User} />
    </div>
  </BrowserRouter >
)

This is useful if our components contain content we want shared across other routes. If needed, we can create exclusive routing in v4 with the addition of a few keywords. Adding an exact prop to a Route means that it only renders for exact path matches. Also we can import a new Switch component which, when wrapped around routes, returns only the first match. This is what an exclusive routing in v4 looks like.

const App = () => (
  <BrowserRouter >
    <div>
      <Route exact path='/' component={Navbar} /> // only visible at `/`
      <Route path='/home' component={Home} />
      <Switch>
        <Route path='/users/:id' component={User} />
        <Route path='/users' component={Users} />
      </Switch>
    </div>
  </BrowserRouter >
)

In the example above, Navbar only renders on the / path because it is looking for exact path matches. I switched the order that the /users/:id and /users routes appear in. Since neither of those routes has the exact prop, they are both looking for the first partial match. That means the /users route would match if the path contained /users/:id. By putting the /users/:id route first, any user page paths are matched before they hit the /users route. Alternatively I could have left the order of those routes unchanged and passed the /users route the exact prop and it would have ignored any /users/:id paths.

Switch and exact give us the flexibility to have exclusive routing when we desire it, but also to avoid repetition by using inclusive routing to render multiple components for the same path.

Route Component

The Route component is how you render content based on a path. To render content, pass it a component, render or children prop. The component property takes a React component reference and creates the element when the route matches. Use render when you want to pass extra props to a component or render something inline. Render accepts a function which returns a React element. Children also accepts a function which returns a React element, but it renders the element regardless of the route matching.

const App = () => (
  <BrowserRouter >
    <div>
      <Route path='/' component={Navbar} />
      <Route path='/home' component={Home} />
      <Switch>
        <Route path='/users/:id' render={() => <User userId={4} /> } />
        <Route path='/users' component={Users} />
      </Switch>
    </div>
  </BrowserRouter >
)

In the above example, the User component can be passed an extra prop of userId because we used the render property. If we were just passing in a reference to a component (as with the component prop), the Route would create the element for us. When we use the render property, we get to create the new element and control what is passed into it. This permeability is what makes the render property useful.

Renamed Route Properties

Elements rendered by Route also get passed route properties including match, location and history. This is worth noting because while previous versions of React Router also passed route properties, they were named and nested differently.

In version 2 URL changes were pushed or replaced on a router object, but this has changed to a history object.

// React Router 2
this.props.router.push({pathname: '/a-new-path'})

// React Router 4
this.props.history.push({pathname: '/a-new-path'})

Accessing variable params from the URL has also changed. This information is now nested under the match object. The match object has the properties url (the matched part of the URL), path (the matched path), isExact (boolean representing if the path exactly matches the route) and params (URL parameters). The url and path properties might look similar to each other, but they differ slightly. The path shows the parameters as they are listed in the route (/users/:id), while the URL shows them as they are listed in the URL (/users/2).

// React Router 2
nextProps.params.userId // returns the userId param from the URL

// React Router 4
nextProps.match.params.userId // returns the userId param from the URL

Lastly search term queries have been renamed from query to search, but are still a property of the location.

// React Router 2
this.props.location.query

// React Router 4
this.props.location.search

Much of the functionality for Route properties is the same, but the names will need to be updated.

Navlink

Our app previously had a custom component which wrapped the React Router Link class and gave the Link element an additional class if its path matched the active route. This meant that for a set of links, we could grant different styling to the active link. React Router 4 now has a Navlink component which accomplishes this same task. It takes the props of a className and an activeClassName to apply to the element when the path matches the link.

Summary

Overall the greatest energy investment for this update went towards rethinking component structure. Decentralized and inclusive routing meant that we had to create some additional container components to assist us with nested routes. At a high level, similar routes were sent to container components and the logic was delegated to the container.

const App = () => (
  <BrowserRouter >
    <div>
      <Route path='/' component={Navbar} />
      <Route path='/home' component={Home} />
      <Route path='/users' component={UsersContainer} />
    </div>
  </BrowserRouter >
)

const UsersContainer = () => (
  <Switch>
    <Route path='/users/:id' component={User} />
    <Route path='/users' component={Users} />
  </Switch>
)

In the above example App delegates all /users routes to UsersContainer which controls all logic having to do with the rendering of user pages.

As with most React problems, the work involved moving components around to get access to the right props in the right places. While the changes took a few days to implement, Router 4 has a much more intuitive structure which feels (as intended) much closer to the core tenets of React. I think we’ll find it much easier to expand and adapt our application to Router 4 in the future.