Upgrading React Router from v3 to v6
We all know it is important to keep our dependencies upto date specially when it comes to the ever changing world of Javascript, but there is always a deadline hanging over your head or the current package is working fine so why bother upgrading it.
Something like this happened to us in Sematext and we kept delaying upgrade of React Router. We were still using React Router version 3 while 6 came out with bunch of enticing features. To give you some context: Router v3.0.0 came out in late 2016 and v6.0.0 came out in late 2021!
By mid 2022 we decided that it was high time we upgraded and I like our readers decided to google how to upgrade from v3 to v6. But all articles I found were focused on upgrading it from v4/5 to v6. Of course who would be crazy enough to upgrade from v3 to v6 😅. This is partly the reason I wanted to write this article so I can share what its like upgrading a complex app from v3 to v6 and how to solve the problems you might face.
Major Changes
There were bunch of API changes between v3 and v6. I highly recommend reading the official migration guide. But as you can see from title, it is focused more on migrating from version 5. So I have compiled a list of major changes that you need to be aware of when upgrading from v3 to v6.
-
Before React Router had its own version of
location.query
and all the libraries that depended on Router utilized this API. But by v6 the Router team had moved to usingURLSearchParams
which is a native browser API. -
In v3 the Route component accepted component as a prop but now element is passed as a prop. This allows you to pass props easily to component in our Route.
-
withRouter
HOC is no longer available in v6. Instead v6 promotes the use of hooks to access any router property you need. -
In v3, components that were passed to the
Route
element by default were passed router props. But now you have to specifically access them either using hooks or passing them as props. -
History package is no longer a peer dependency of React Router. Instead it is a direct dependency and its versioning is handled by React Router team. In fact you are recommended not to install it at all so that there are no conflicts.
There are a lot of other changes like Switch
(introduced in v4) is now called Routes
and Redirect
is now called Navigate
, plus amazing API's
like loaders and actions were introduced. But the main focus of this article is
how we tackled the major challenges we faced in our large production app.
Our Specific Challenges
Creating your own withRouter HOC
In older apps its common to see old class based components living alongside new functional components. Our app still has many legacy class based components meaning that we still needed access to the withRouter HOC. But now its not natively provided. So this is one of the first things we had to do.
Using this HOC we were able to access router props in our class based components.
Navigating outside of Router's context
In React Router you can not use hooks like useNavigate
outside of Router's context. But it
is common for apps to have navigation in things like Redux Thunks. This is one of the most difficult
things to solve in v6. You can get an idea how contentious this issue is by looking at the
Github issue for it. People from the core team
have suggested different solutions including using things like unstable_HistoryRouter
. So for this
important use-case we decided to use our novel approach which works great for us.
We basically decided to rely on native JS custom events and event listeners. You can see what the code looks like below:
We are using the eventemitter3
package but you don't need it to accomplish the same. With this approach
you are free to navigate anywhere in your app by just calling mainRouterNavigate(to, options)
and
making sure that <RouterEventListener />
is inside Routers context. The one advantage of
using this approach compared to the ones mentioned in official repo is that you are free to
use the latest data router and you avoid any cyclic dependencies.
Nesting Routers
Our project utilized more than one router. In fact the same routes are shared across multiple routers so that you can create a single page that you can open in the main view, split view or in a flyout. Whereas most of these are direct children of the parent component but split view router is part of the main router. Before you could nest routers but that is now prohibited. So how do we solve this problem?
Enter React Portals! We create the split view router at the parent component as well but its content is rendered inside the main router using React Portal.
Passing Data in Routes
Pre-upgrade we relied heavily on passing data in routes. For example all our breadcrumbs were constructed by passing specific breadcrumb to each relevant route in a path. In v3 it was easy for us to just append whatever data we needed for that route in the actual route configuration. But when v6 initially launched it had no such facility. But with the introduction of data router they introduced handle property plus useMatches API for accessing route specific data. Now we have Routes like this:
Then you can you have a breadcrumb component like so:
Solving circular dependencies
Because this blog is about upgrading a large app, from v3 to v6, it means that you will have some legacy in your codebase. And if your app is structured in a way that you have to import the same thing in multiple places there is a possibility that you will run into circular dependency issues. You can also run into this issue if you decide to import your main router object in different files for navigation.
To solve circular dependency the correct approach is to refactor your codebase so that you import the common stuff from a single file. With correct modularization you can avoid this issue altogether.
But that might not be possible for your codebase or it may require a mammoth effort in an already large router upgrade. So as a last resort I will reluctantly suggest the following approach where we create a router object initially and inject the the newly created router object into it with the following approach:
I would reiterate that this should be a last resort, either you should not have circular dependencies or you should refactor your codebase to avoid them. But if you get stuck like we did then you can look at this approach. (I am not proud of it 😅)
Conclusion
In this blog post I have tried to share the major pain points of upgrading from v3 to v6 and how to solve those. React Router v6 is a great upgrade and with new data routers it has opened a lot of useful features. Hopefully this article can help people who like us who were stuck on v3 still. Happy coding! 🚀