The Motivation
When working on the next release of ezploy.io (currently moved to stackmate.io), among the many changes I’ve introduced, I had to upgrade react-router to its latest stable version v4. At first I thought it was a bit of a luxury since I tried to roll out this version for a long time, but then again I thought that it would provide great benefits in terms of security, stability, flexibility and speed, so I went ahead and spent that 3-4 hours anyway; here’s the story behind that.
First things first
You have to carefully read the Official migration guide that the awesome contributors of React Router have crafted.
Key Concepts
The first thing you need to know is that the react-router
is broken down to several packages that you need to additionally install, since the react-router
package is designed as a core package, working both with React and React Native. This is the main reason why you need to familiarize yourself with the react-router-dom
package, which you’ll be using from now on.
Second, there’s no need for having all of your Routes in one file. The routes can now be hosted inside components so you need to refactor your main application’s component and your existing routes file; Your main application component should host all the top-level routes from now on and your (existing) routes file should now feature an array of routes (more on that on the next paragraph).
Third, if you’re using onEnter
, onChange
and other hooks like setRouteLeaveHook
, you may need to perform a deeper dive on the React Router documentation, and perhaps a quick look at this thread too, as these have now been removed. There’s a section at the end of this post explaining what you need to do if you require user confirmation when navigating away from a page (ie. unsaved changes etc).
Fourth, if you’re passing params
in your data prefetching functions or examining params
in your props, keep in mind that params
is now a property of match
which is the router match object (which we’ll talk about in a bit).
Last but not least you may need to spend some time refactoring your imports, since the <Link>
component, or the withRouter
wrapper for example, are now located in the react-router-dom
package.
Isomorphic rendering & data prefetching
In order for the server side rendering to work properly we need to have all data pre-fetched and the Redux store hydrated (if you’re using Redux on your stack). This is achieved by having each data-fetching component set up as follows:
Notice how we pass match
as the second argument of the fetchData
function, this used to be router’s params
in previous versions, but since params
is now a property of match
, we pass the match
object instead.
Now, remember when we said we don’t need to have a central place for your routes? That’s sort of true; Not having all your routes in one place, means that each component should be able to declare Routes inside its render
function for example, meaning that isomorphic rendering with data prefetching becomes a lot trickier. Not anymore, because now’s the time to install react-router-config
and setup a fairly big array of objects, containing all the routes in the system (hence the “sort of true” about this argument), much like that:
Having done that, you may now use renderRoutes
and matchRoutes
on your server side router. You’re going to match the route based on the url the user is currently at, then get the components that this route uses, apply the fetchData
function and done!
The most common pattern for doing server side rendering, is having a middleware (in our case an Express.js middleware) which renders on all urls (catch-all) and delegates the actual routing to react-router.
If what I’ve mentioned above sounds familiar to you, your existing code (pre-upgrade) probably uses matchPath
function, like this one:
matchPath(req.url, routes).then((error, redirectLocation, renderProps) => {
// I can has server side rendering in here
});
It’s now time for that matchPath
function to retire and match your routes with the matchRoutes
function, provided by react-router-config
. Here’s the gist of how my server side router looks after applying the changes (make sure you follow the comments in the code):
Done! Your app now renders isomorphically, with all the data pre-fetched and the Redux store hydrated. We’re not entirely done though, let’s just make sure that our client app is up to date, here’s the gist again:
Extra things to consider:
- When using nested routes like for example
/projects/10
and then/projects/10/member/1
, you may need to mark the first one asexact
, otherwise you’ll end up resolving unexpected components - On the same subject, let’s say we have the following route set up
and inside the Project
component you have the following route set up:
When visiting /projects/5/collaborators/15
, you may not be able to access the :component_id
param inside the Project
component due to this issue.
-
If you’ve been using the
setRouteLeaveHook
hook to prompt your users before navigating away from a page, you can use the getUserConfirmation. Now, if you need a bit more granularity prompting your users when navigating away from a page, consider using the Prompt component, while if you need to render a custom React component or perform a custom hook when confirming / canceling navigation, there’s a nice replacement for that. -
If you’re using
react-router-redux
in order to navigate likedispatch(push('/my-url'))
, you need to upgrade to itsnext
version, as the project has been moved as well. They also provide a comprehensive example in their documentation which features theConnectedRouter
component in order to automatically connect to the store object.