Upgrade React Router 3 to React Router 4

Not  long after I started working at my current position, the team realized  that it would be necessary for us to upgrade to React 16 in order to use  a new UI library we were keen on switching to. To figure out how much  time this upgrade would require, we looked at all of our current  packages to see if they were compatible with React 16, and to see if we  were still using unsupported or deprecated packages. The beginnings of  our code base had been built by developers who used whatever open source  or third party library they wanted, without actually vetting them.  Thus, we found that a lot of the packages were deprecated and needed to  be replaced ASAP. One of the biggest surprises for us was the deprecation of react-router-redux. We were using react-router-redux in  conjunction with react-router v3. This led us to think critically about  why we were using redux in our router in the first place. Once we  started to look into react router v4, we realized that the new features  would pretty much eliminate any reason for us to use an additional  library to connect our router and redux. So, that left us in the  position to just upgrade from react router 3 to 4, and remove  react-router-redux from our application.

So,  I was tasked with upgrading our router to v4 after only being in the  position and working with React for only about 2 months. This was because upgrading from React Router 3 to React Router 4 sounded like it should be a trivial undertaking; however, as I quickly found out, it was  a little bit more involved than I anticipated. Looking through the documentation, the GitHub repo,  and many, many Stack Overflow answers, I finally pieced together the  steps for the upgrade and wanted to share my findings — especially to  explain how and why certain changes are made.

The  biggest change to note, from the creators of React Router, is that the  upgrade from React Router 3 to React Router 4 is more than just updating  a few libraries and features — it allows you to fundamentally change  how your Router works. The creators of React Router wanted to go back to  a simple Router, allowing the developer to customize it however they  would like.

I’ve split up this guide into 5 different parts:

  1. Package
  2. History
  3. Route
  4. Props
  5. Redux Integration


React  Router v4 package structure changed such that it’s no longer necessary  to install react-router — you have to install react-router-dom (and  uninstall react-router), but you don’t lose anything since it re-exports  all of react-router’s exports. This means you also have to update any  react-router import statements to react-router-dom.


History  is an essential part of routing, allowing us to remember where we came from and where we are currently. History comes in many forms for  react-router, and could take a while to explain. So, to keep this article on topic, I’ll simply recommend that you read through this article that explains history in relation to react router 4. This article should cover most cases of your usage of history.


React  Router v3 allowed us to place all of our application routes in one  file, which we’ll call router.js. However, React Router v4 allows you to  place Routes in the components that they’re rendering. The idea here is  to dynamically route the application — in other words, the routing takes place as the app is rendering.

However,  if you have a decent-size legacy code base you’re working with, you probably won’t be making a change that big. Luckily, React Router v4  still allows you to place all the routes in a central file, which is how  I’ll create all of our examples. There are, however, a few older  components and features that will need replacing.

IndexRoute

Previously,  IndexRoute was used as a route for some default UI of a parent Route. But, in v4, IndexRoute is no longer used, since this functionality is  now available in Route.

For providing default UI, multiple Routes that have the same path will let all of the associated components render:

import { BrowserRouter as Router, Route } from 'react-router-dom';

<Router>
    // example of our route components
    <Route path="/" component={Home} />
    <Route path="/" component={About} />
    <Route path="/" component={Contact} />
</Router>

So, all of the Components — Home, About, and Contact — will render. Because of this, you can no longer nest Routes, either.

Additionally, to allow for better matching without the use of IndexRoute, you can use the exact keyword.

import { BrowserRouter as Router, Route } from 'react-router-dom';

<Router>
    // example of our route components
    <Route exact path="/" component={Home} />
    <Route path="/about" component={About} />
</Router>

Exclusive Routing

After adding in the exact keyword, “something.com/about” will be routed to  when the router sees a path “/about”. But now what if you have another  path, “/about/team”? As I stated before, the router will render anything  that matches. So, the components associated with both “/about” and  “/about/team” will render. If that’s what you intended, then that’s great! However, if this isn’t what you want, you may have to put a  Switch around this group of Routes. This will allow the first path that  matches the URL to render.

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

<Router>
    <Switch>
       <Route exact path ="/" component={Home} />
       <Route path="/about/team" component={Team} />
       <Route path="/about" component={About} />
    </Switch>
</Router>

Note  that the keyword exact still has to appear for the Home component —  otherwise it would match for the subsequent routes. Also note that we  have to list “/about/team” before “/about” so the route goes to the Team component instead of the About component when it sees  “something.com/about/team”. If it saw “/about” first, it would stop  there and render the About component because Switch only renders the  first component that matches.

Default Route

A default route, or a “catch all” route, commonly used for 404 pages, is the route you use when none of the routes match.

In React Router v3, a default Route was:

<Route path=”*” component={NotFound} />

In React Router v4, the default Route was changed to:

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

<Router>
    <Switch>
       <Route exact path ="/" component={Home} />
       <Route path="/about/team" component={Team} />
       <Route path="/about" component={About} />
       <Route component={NotFound} /> // this is our default route
    </Switch>
</Router>

When  you don’t include a path in a Route, the component will always render.  So, as we discussed above, we can use Switch to get only one component  to render, and then place the “catch all” route very last (so it doesn’t  use that one before the Router gets a chance to check the rest of the  paths), so something will always render even if the other paths don’t  match.

onEnter

Previously,  you could use onEnter to make sure the component of the Route has all  of the information it needs or as a check (such as to make sure the user  is authenticated) before the component rendered.

This  feature has been deprecated because the new structure of Routes is that  they should act like components, so you should take advantage of component lifecycle methods instead.

In React Router v3:

<Route path=”/about” onEnter={fetchedInfo} component={Team}/>

Becomes:

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

<Router>
    <Switch>
       <Route exact path ="/" component={Home} />
       <Route path="/about/team" component={Team} />
       <Route path="/about" component={About} />
       <Route component={NotFound} />
    </Switch>
</Router>
router.js

...

componentDidMount() {
    this.props.fetchInfo();
}

...
Team.js


In  React Router v4, the props passed through the router have changed, as  did the way they are accessed. The Route now passes three props:

history

history contains a lot of other properties and methods, so I won’t list all of  them, but here is a selection that might be most commonly used:

  • length: number of entries in the history stack
  • location: contains the same information as below
  • push(path, [state]): pushes new entry on history stack
  • goBack(): allows you to move the pointer on the history stack back 1 entry

It’s  important to note that history is mutable, and while it contains a location property, this instance of location shouldn’t be used since it  could have been changed. Instead, you want to use the actual location prop discussed below.

location

The location has properties:

location.search is used to replace location.query and it must be parsed. I used  URLSearchParams to parse it. So a URL such as  “https://something.com/about?string=’hello’” would be parsed as such:

...

const query = new URLSearchParams(this.props.location.search)
const string = query.get('string') // string = 'hello'

...
About.js

Additionally,  the state property can be used to pass the location-specific state of components through props. So, if you wanted to pass some information  from one component to another, you could use it like this:

...
// To link to another component, we could do this:
<Link to='/path/' />

// However, if we wanted to add state to the location, we could do this:
const location = {
    pathname: '/path/',
    state: { fromDashboard: true },
}
<Link to={location} />
...

So, once we get to the component rendered by that path, we’ll have access to fromDashboard from location.state.fromDashboard.

match

match has the following properties:

  • params:  gets the dynamic segments of the path from the URL — for example if the  path is “/about/:id”, in the component, accessing  this.props.match.params will give you the id in the URL
  • isExact: true if the entire URL was matched
  • path: the path in the routes that was matched
  • url: the matched portion of the URL

As  I addressed earlier, we found that in our case, we didn’t need to have an additional library to connect redux with our router, especially since our main use case for this — Blocked Updates — was covered by react  router.

Blocked Updates

In  some cases, the app doesn’t update when the location changes. This is called a “Blocked Update”. This can happen if both of these conditions  are met:

  1. The component is connected to Redux via connect()(Component).
  2. The component isn’t rendered by a <Route>

In these cases, I wrapped the component’s connect with withRouter.

This  allowed the router information to follow the component when it gets linked to, so the app still updates when the Redux state changes.


And that’s it!

This  upgrade took me over a week — a few days of trying to figure out how to  do it at all, and then another few days to start actually making  changes. Upgrading to React Router 4 is a huge change not to be taken  lightly, but it’ll make your router a lot more lightweight and easy to  use!

Please don’t hesitate to comment/ask questions!