While working on the new Envato Market Shopfront app, the team agreed to always keep all the dependencies in the project up to date. Sometimes it was just straightforward patch or minor version upgrade, but sometimes it could also be breaking changes that need a whole lot of thought. The upgrade to
react-router v4 happended to be a good example.
First a little bit background on our current stack (and version):
- React for the view layer (v15.4.2)
- Redux for state management (v3.5.2)
- React Router for routing (v2.8.1)
- Node.js for server-side rendering and providing a simple proxy layer to our APIs (v6.10.1)
My original plan was to upgrade all the dependencies in one pull request. But when it comes to the React related package families, things start to get out of control. For those who are using React in their project already, you may have already heard about the latest changes to React v15.5.0.
The biggest change is that we’ve extracted React.PropTypes and React.createClass into their own packages.
This means, for every single component that is using those two packages or methods, it will have to be updated to using the new packages to get rid of all the deprecation warnings. Luckily, the React team always provide nice codemod with react-codemod to automatically migrate the code.
But what about third party React related modules? If you’ve chosen your project’s packages wisely and with a little luck, the package author would have already released a new version to support the latest release of React and, even if that’s not the case, this might be a good opportunity to give back by sending a pull request to the repo.
Everything went pretty smoothly until it came to upgrade React Router. We are currently on v2.8.1, do we want to upgrade to v3 or v4 now?
Considering all the changes we’ve already made to the other React packages, I thought that there might be too many changes in one pull request, so in the end I decide to try to only update to v3 (as I’ve heard React Router v4 has changed dramatically since the previous version) in a separate pull request.
It seems to me that the biggest change from v2 to v3 for React Router is
withRouter according to the change logs.
routesto props injected by
withRouterand to properties on
It turns out to be a big problem for us because we depends on the location object heavily for critical search query filters, SEO and other stuff. Previously,
location was not injected by
withRouter, we were passing a modified version of it from the very top page level down to component which need access to
And not by coincidence, those components also use
withRouter to do
props.router.push for page transitions (router here is injected by
withRouter to component props). The newer version however providing that, we will now have lots of conflicts regarding the
Because the code is heavily dependent on React Router and we can’t change the internal API provided by it, we can only modify or rename the
location we are passing down which is not a small amount of work.
Considering the amount of work from v2 to v3, why not upgrade to v4 directly? I decided to have a try.
Before reading this I highly recommend reading the migrating guide from the official Github Repo first.
The first change I made is to install the new
react-router-dom and update all the references in the code from
react-router-dom. For those who don’t know what
react-router-dom is and the difference between them, short answer is that
react-router includes both
react-router-native. For a web based project
react-router-dom is usually what you need.
Another big difference is that instead of having a centralised route configuration for your application and rendering children based on router, now you can define a child component as a normal one inside the component where you need to render content based on current location.
However this doesn’t really work for us because we are doing isomorphic rendering. The key for achieving isomorphic rendering is the ability to pre-fetch data before calling
React.renderToString so the content(HTML) you sent to browser will have the required data.
In the previous version, we normally have a central routes config like this
1 2 3 4 5 6 7 8 9 10
On the server side, when the request comes in, we can have an express.js middleware like this to handle and render the content
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
What about v4?
In v4, there is no centralized route configuration. Anywhere that you need to render content based on a route, you will just render a
There’s no central routes config anymore, how do we co-locate the static
loadData method on the render component tree?
Luckily there is someone already doing this for us! There’s a package named
react-router-config from the react-router team.
To achieve the same purpose, now we just have to adjust our routes config into something like this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
And in the client side, load it like this
1 2 3 4 5 6 7
And on server side, load it like this
1 2 3 4 5 6 7 8 9 10
With this setup in place, we are now ready to co-locate fetch calls, we can use the matchRotues provided by the package
1 2 3 4 5 6 7 8 9 10 11 12
And 💥, we now have server side data pre-fetch working with React Router v4. The last thing we need to fix is client side data fetching. This happens when user switches route inside the browser, we will also need to trigger the same requests to load new data to render new content.
In the previous version, we can use
browserHistory.listen to watch client router change and trigger the network request
1 2 3 4 5
This could still work with the new version, but we can also follow the example given in
react-router-config repo to create a special component, and use
withRouter to attach the
location object to the compoent
props, then we can use
componentWillReceiveProps to listen on location change and trigger the network request call.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Another benefit of using
browserHistory.listen is that you have a context of the previous location and the current location so you can implement
shouldFetchNewData to prevent making expensive network requests.
Voila! That was pretty much what we needed to do to upgrade an isomorphic React app from React Router v2 to v4. I’ve definitely learned a lot from it:
- It was probably a bad idea to depend so much on a routing library so deeply nested in application component tree. What we should probably do next is make the location part of the redux store—this way, the next time the location object changes, we simply update it in the redux store without having to modify things all over the code base.
- Some of you who read this article may be wondering why am I doing the upgrade, same question for me when I was in the middle of doing this. Is it just because we want to keep everything up to date? I’m not sure. Maybe not. I was able to figure out that there are still quite a lot of places in our code which could be improved.
Last but not least, I wrote this article because by the time I went to do the upgrade, I couldn’t find any existing example I could refer to, and I hope you may find this one helpful.