Migrating to React Router v6: A Complete Guide

Migrating to React Router v6: A Complete Guide

Editor’s note*: This React Router migration guide was last updated on 25 October 2022 to include information about* useNavigate vs. useHistory, changes to NavLink, and more information about React Router v6.

When we maintain React apps, we’ll need to upgrade dependency libraries by changing our existing codebases over time. For example, upgrading the React Rounder v5 dependency to v6 requires step-by-step changes in your existing codebase. It may be challenging to transition from React Router v5 to v6 because several changes have altered how things were done in React Router v5. In addition, several new features have been introduced, so it is recommended to upgrade to v6 even if the transition is slightly annoying.

To upgrade from React Router v5 to v6, you’ll either need to create a new project or upgrade an existing one using npm. React Router v6 also extensively uses React Hooks, requiring React v16.8 or above. In this article, we’ll look at issues with React Router v5, what changed, how to upgrade to v6, and what benefits this upgrade offers.To follow along, you should be familiar with React Router.

Issues with React Router v5

React Router v5 came close to perfection, but there were still some flaws. The v5 library has some issues with the routing algorithm and routing configuration defaults (JSX component prop defaults). Also, the v5 came with some inconsistent, somewhat complex APIs that affect the developer’s productivity. Moreover, v5’s bundle size weighs more compared to what it offers as a React library. Here is a detailed explanation of the major issues that every developer faces in React Router v5:

Path-matching issues

Routes queries route paths with the most similar naming instead of with exact naming. For instance, a route name /dashboard would still be called if the browser route is /dashboard/test, or /dashboard/testagain, etc., which is not expected by app developers. Developers have to explicitly specify the prop exact to strictly query route names.

Also, defining routes with the same path but optional parameters requires special arrangements. For instance, the parameter is required when defining the route /games and another route with the same name. Thus, developers need to arrange the definition, so the route with the parameter comes first. Otherwise, the routes won’t work as expected because of the issues in v5’s routing algorithm.

Look at the following example that demonstrates the ranking problem in the v5’s routing algorithm:

The correct way: Here, /games will render the Games component but not SelectedGame, because it does not have a parameter id. While /games/:id will render only the SelectedGame component:

<Router>
  <Route path="/games/:id" component={SelectedGame} />
  <Route path="/games" component={Games} />
</Router>

The incorrect way: Here, either /games or /games/:id will render the Games component:


<Router>
  <Route path="/games" component={Games} />
  <Route path="/games/:id" component={SelectedGame} />
</Router>

The above code snippets illustrate the right and wrong way to order routes that are related by paths or parameters according to React Router v5. The first example is the most suitable order, while the second allows only the route /games to render for any situation with a parameter.

According to v5’s perspective, the second approach is incorrect, and developers need to use the exact prop or re-order (as in the first code snippet) to fix it. However, the v5 route ranking algorithm could be intelligent enough to identify the most suitable route without explicit ordering or exact usage.

For developing nested routes, developers had to write more code with the useRouteMatch Hook, Route, and Switch components. For nested routes, developers had to find the entire route because there is no relative route handling in v5:

// ----
<Switch>
  <Route path="/dashboard" component={Dashboard} />
</Switch>;
// ----

function Dashboard() {
  const { path } = useRouteMatch();
  return (
    <div>
      <h1>Dashboard</h1>
      <Switch>
        <Route path={`${path}/charts`} component={Charts} />
      </Switch>
    </div>
  );
}

Issues in API consistency and simplicity

When a specific library offers inconsistent and somewhat complex APIs, application codebases become less maintainable and more error-prone. So, software library developers always try to offer consistent and simple APIs for app developers to manage their codebases better. However, library developers can’t offer the most optimal API design for app developers with their first release. API improvements typically come with the open-source developer community’s feedback, bug reports, and feature requests.

React Router v5 has some inconsistent APIs and unfamiliar naming. Also, app developers had to write more boilerplate code than they expected to configure and integrate React Router v5 into their apps. For example, for programmatic navigation, v5 offers the useHistory Hook, but almost all React Router newcomers expect useNavigate.

Also, the Route component offers two different props to link components: component and render, but it’s possible to offer one element prop as the Suspense API does. Moreover, for defining your routes with JavaScript objects in v5, you have to use the separate react-router-config package.

Excessive bundle and code size

As we already know, when we use heavy dependencies, our applications also become heavy. Similarly, if we use a library that requires writing verbose API calls, our application bundles also contain excessive code, which leads to heavy bundle sizes.

React Router v5 latest stable release (v5.3.4) ‘s bundle size is about four times larger than React!Having considered the issues with React Router v5, we will now discuss how to migrate and what has changed that makes React Router v6 different.

Migrating to React Router v6

The following sections will teach you how to upgrade to React Router v6 in projects where React Router is already installed and from scratch.

Upgrading React Router in a project where it is already installed

To upgrade the React Router version, open a terminal and navigate to the project directory where you wish to upgrade it.

To see a list of the project’s dependencies, use the following command:

npm ls
# --- or ---
yarn list

You should see a list like this:

Although the list generated may not be exactly the one above, you should see an entry with its installed version if you have React Router installed.

Next, run the following command to initiate an upgrade:

npm install react-router-dom@latest
# --- or ---
yarn install react-router-dom@latest

If you execute this command without being connected to the internet, it will fail because some files must be downloaded during the installation. If everything is in order, you should see something similar; in our case, the version is v6.0.2:

If everything goes well, we should notice that React Router has been updated when we run the npm ls or yarn list command again:

Installing React Router from scratch

First, open a terminal in a project directory where React Router isn’t installed.

To install a specific version of React Router, run the following:

npm install react-router-dom@[VERSION_TO_BE_INSTALLED]

Replace [VERSION_TO_BE_INSTALLED] with the version you want to install, for example, 6.0.2.

Or, run the following code to install the newest version:

npm install react-router-dom
# --- or ---
yarn install react-router-dom

This installation also demands the use of the internet. If the installation went well, you should see something similar to this:

What’s changed in React Router v6?

It’s important to understand what changed so we can see why upgrading to React Router v6 is helpful. Note that in certain circumstances, developers will downgrade a program to increase functionality or avoid issues.

We will go through the changes in React Router v5 that one should consider when choosing which version to implement in a project.

Setting up routes

We had three different techniques for generating routes in React Router v5, which caused confusion. The first technique is to pass the component and path as props of the Route component:

<Route path="/games" component={Games} />

This works well, however, we cannot pass props to the rendered component. The second is to pass in the component as a child of the Route component:

<Route path="/games">
  <Games count=”10” category=”Action” />
</Route>

We may pass custom props to the component we want to render using this approach. The third and final technique is to use the render prop where a function returns the component to be rendered:

<Route path="/games" render={(props) => <Games {…props} />} />

This also works and lets us give props to the component we’re rendering. However, it is ambiguous and prone to inaccuracy.

In React Router v6, routes have been simplified to the point that we no longer need to utilize Switch to query them. Instead, we utilize a “required” component called Routes, which only searches for routes by name. The * character can be used to query using a wildcard.

We then supply the component to be rendered to the Route component as element props. We can also supply custom props to the components in each route we wish to render.

The code snippets below demonstrate how to define routes in v6:

<Routes>
  <Route path="/games" element={<Games />} />
  <Route path="/movies" element={<Movies genre="Action" age="13" />} />
</Routes>

You can also use useRoutes to query the routes in the app. To do this, we must alter the index.js content, where we change the App wrapper from React.StrictMode to BrowserRouter as below:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { BrowserRouter } from 'react-router-dom'

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);

Then, in the App.js file, we define the routes by performing the following:

import { Outlet, useRoutes } from 'react-router-dom';
const App = () => {
  const routes = useRoutes([
    {
      path: '/',
      element: <div>Hello Index</div>
    },
    {
      path: 'games',
      element: <Games />,
      children: [
      {
        path: '',
        element: <div>Games Index</div>
      },
      {
        path: ':id',
        element: <div>Game Details</div>
      }]
    }
  ]);
  return routes;
}

const Games = () => {
  return (
    <div className="Games">
      <div>This is the Games page</div>
      <Outlet />
    </div>
  );
}

export default App;

Here, we imported Outlet and useRoutes. The useRoutes allows us to define the routes as an array of objects in which we can specify a path, the element to be rendered when the path is browsed, and sub-paths. Outlet helps us render the child route that matches the current path.

useNavigate vs. useHistory

In React Router v5, we use useHistory() for handling navigation programmatically. There have been concerns with this technique, such as naming confusion and having two methods for navigation, history.push and history.replace.

To implement navigation in v5, we usually do the following:

import { useHistory } from 'react-route-dom';

const App = () => {
  const history = useHistory();
  const handleClick = () => {
    history.push('/home');
  }
  return (
    <div>
      <button onClick={handleClick}>Go Home</button>
    </div>
  ) 
}
export default App;

We use the history if we need to replace the current history frame.replace('/home') method call.

In v6, we use useNavigate instead of useHistory:

import { useNavigate } from "react-router-dom";

const App = () => {
  const navigate = useNavigate();
  const handleClick = () => {
    navigate("/home");
  }
  return (
    <div>
      <button onClick={handleClick}>Go Home</button>
    </div>
  );
}
export default App

Instead of history.replace, we can call the navigate method with an options object as follows:

const handleClick = () => {
  navigate('/home', { replace: true });
}

So, what happened to the go, goBack, and goForward methods in the legacy history API in v5? In v6, we can call the same navigate function with numbers. For example, look at the following code snippet that demonstrates go back, forward, and 2nd history frame navigation:

navigate(-1)   // v5's history.go(-1) or history.goBack()
navigate(1)    // v5's history.go(1) or history.goForward()
navigate(2)    // v5's history.go(2)

Also, v6 replaces Redirect with a declarative version of useNavigate, Navigate:

import { Navigate } from 'react-router-dom';

function App() {
  return <Navigate to='/home' replace state={state} />;
}

Moreover, the navigate API is now suspense-ready. You can inspect a sample suspense-based router implementation from this source file on GitHub.

The v6 versions also changed the NavLink component interface that helps you create breadcrumbs, tabs, and navigation menus with dynamic styles. This is a frequently used component in most React Router-based apps, so you may have to allocate more time for NavLink related rewrites.

In v5, we use the prop when enforcing an exact path or route. To implement this, we do the following:

<NavLink to="/dashboard" exact>Go Home</NavLink>

In v6, we use the end prop to ensure exact routes:

<NavLink to="/dashboard" end>Go Home</NavLink>

The React Router team renamed this prop to align the library with the React ecosystem’s practices.

In React Router v5, we use the activeClassName or activeStyle props to style a NavLink that is currently active.

For instance:

<NavLink 
  to="/home" 
  style={{color: 'black'}} 
  activeStyle={{color: 'blue'}} 
  className="nav_link" 
  activeClassName="active" >
  Go Home
</NavLink>

In v6, we have to use a function with the destructured argument {isActive} to condition the style or className to be used for an active NavLink.

For instance:

<NavLink 
  to="/home" 
  style={({isActive}) => ({color: isActive ? 'blue' : 'black'})} 
  className={({isActive}) => `ln-${isActive ? ' active' : ''}`} >
  Go Home
</NavLink>

These changes also fix the consistency and simplicity issues in the v5 legacy API. However, if your React app uses the above-deprecated props extensively, you can make your migration process faster and smoother by creating a temporary wrapper for v6’s NavLink as follows:

import * as React from 'react';
import { NavLink as BaseNavLink } from 'react-router-dom';

const NavLink = React.forwardRef(
  ({ activeClassName, activeStyle, ...props }, ref) => {
    return (
      <BaseNavLink
        ref={ref}
        {...props}
        className={({ isActive }) =>
          [
            props.className,
            isActive ? activeClassName : null,
          ]
            .filter(Boolean)
            .join(" ")
        }
        style={({ isActive }) => ({
          ...props.style,
          ...(isActive ? activeStyle : null),
        })}
      />
    );
  }
);

The above code converts the v6 NavLink to a v5-like NavLink and lets you deploy the app with v6 by keeping working v5-styled NavLinks that you can re-write without a rush.

Use useMatch instead of useRouteMatch

In v5, useRouteMatch was used to create relative sub-route paths that matched a particular route. We can use this Hook with or without the pattern argument as follows:

// Receive the matched path and url of the current <Route/>
const { path, url } = useRouteMatch();

// Receive the matched route details based on the pattern argument
const match = useRouteMatch('/users/:id');

In v6, we use useMatch for this. Using the useMatch Hook requires a pattern argument and does not accept patterns as an array. So, the following code snippet is valid for v6:

const match = useRouteMatch('/users/:id');

However, v6 throws an error if you don’t pass the pattern argument to the Hook. The above match constant will receive details about the route match.

Relative routes support for nesting

In v5, creating JSX-based nested routes generated more boilerplate code segments since we had to construct full URLs explicitly. Consider the following example code snippet:

// ----
<Switch>
  <Route path="/dashboard" component={Dashboard} />
</Switch>;
// ----

function Dashboard() {
  const { path } = useRouteMatch();
  return (
    <div>
      <h1>Dashboard</h1>
      <Switch>
        <Route path={`${path}`} component={Summary} />
        <Route path={`${path}/stats`} component={Stats} />
        <Route path={`${path}/charts`} component={Charts} />
      </Switch>
    </div>
  );
}

Here we need to manually construct the full path for each’s path prop using the useRouteMatch Hook. So, when you develop complex apps, you can see this boilerplate code everywhere.

React Router v6 made JSX-based nested routes definitions so minimal. Look at the following v6 re-write of the above v5 sample code snippet:

// ---
<Routes>
  <Route path="/dashboard" element={<Dashboard/>}>
    <Route path="" element={<Summary/>}/>
    <Route path="stats" element={<Stats/>}/>
    <Route path="charts" element={<Charts/>}/>
  </Route>
</Routes>
// ---

function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Outlet/>
    </div>
  );
}

Here v6 lets developers use the Route component in a nested way to define nested routes. Note the Outlet component that renders nested components: Summary, Stats, and Charts. Here we don’t need to prepend /dashboard/ to all nested routes, unlike v5, since v6 supports relational path definitions.

What was removed from React Router v6?

In React Router v6, some features are no longer supported because they are ambiguous or faulty. So far, two features were found to have been removed:

Prompt, usePrompt, and useBlocker

  • usePrompt (JSX Prompt): Was used to confirm whether a user wants to exit a page when the page is in a state that does not require the user to leave

  • useBlocker: Similar to usePrompt, and was used to prevent users from leaving a particular page

These two features are similar and have the same issue. In a situation where a user tries to navigate outside a page and navigation is restricted by either usePrompt or useBlocker, the URL path changes even though the navigation is prevented.

Although there were issues raised about useBlocker and usePrompt, the creators are still considering adding them back to v6 after they release a stable version. Check here for more details. Meanwhile, the developer community created libraries like react-router-prompt for app developers who need these removed features in React Router v6.

Nested routers support

Using one BrowserRouter component is enough for developing advanced React app navigations. Some app developers who used React Router v5 practiced using nested routers in their apps to isolate complex components. For example, most developers used v5’s MemoryRouter inside the BrowserRouter instance to insolate modals and tab groups. Unfortunately, v6 dropped supporting nested routers and motivated app developers to use only one preferred router implementation.

Most app developers who built nested routers now request the deprecated feature back in v6 to upgrade their v5 React Router dependency to v6. Some developers worry about this breaking change in v6 and even consider migrating to another React routing module.

According to this discussion, it seems that maintainers are not focusing on nested router support during the current development timeline. But, the developer community may support maintainers to at this deprecated feature soon! Or, maintainers will offer a production-friendly workaround for this issue.

The developer community found the following workaround (rather a hack) to enable nested routers in v6, but it’s not production-friendly due to unexpected behaviors:

<UNSAFE_LocationContext.Provider value={ null }>
  <MemoryRouter>
    { /* some routes not related to the outer router */ }
  </MemoryRouter>
</UNSAFE_LocationContext.Provider>

Subscribe to issue #7375 on GitHub and get updates related to the v6 nested routers.

Why doesn’t React Router v6 work in your app?

You might notice unexpected behaviors, error messages, and warning messages once you complete the migration process. You may have to solve a bug due to a breaking change in v6. We can’t ship apps to production with routing issues. So, detecting routing issues with a string test plan is better.

Your React Router integration won’t work correctly after upgrading to v6 because of the following issues.

The history.push method won’t work properly

You’ve probably updated the history package to the latest without updating React Router to v6. The v5 history package is compatible with React Router v6, not React Router v5. To solve this, first, update React Router, then it will automatically download a compatible history version. If you still need to run React Router v5, use history v4 (i.e., v4.10.1). See this issue.

Export x was not found in react-router-dom

You are importing a Hook or component that was removed or renamed in v6. For example, useRouteMatch won’t work in v6. To solve this, make sure that you don’t import deprecated v5 Hooks or components.

Nested routes won’t render components

Make sure to add <Outlet/> in the parent component that embeds nested route components. Check the previous Dashboard component as a reference implementation.

x is not a <Route> component

In v6, Routes can contain only Route or React.Fragment children, so make sure to move other HTML elements and components to suitable places from the Routes component.

meta.relativePath.startsWith is not a function

You are using an object in the path prop of Route. In v6, you can’t use Regex objects or arrays as a path. Make sure that you use only strings for paths.

Here we discussed some frequently occurring issues. But, you might get a unique error message you can’t find on the internet. Inspect your codebase thoroughly for deprecated API usage in such scenarios, then open a new discussion thread in React Router.

Benefits of React Router v6 over v5

It’s pointless to migrate if one version doesn’t offer any advantages over the other. In this section, we’ll talk about the benefits of upgrading from React Router v5 to v6.

One of the most important considerations for developers is the application’s portability. The size of React Router v6 has been reduced significantly compared to previous versions. The difference between v5 and v6 is around 60 percent.

Here is a comparison from Bundlephobia:

Compiling and deploying apps will be easier and faster due to the size reduction. In addition, the bulk of the modifications in React Router v6 that we covered are more beneficial and have fewer issues.

As previously mentioned, React Router v6 allows routes to accept components as elements when constructing routes, and pass custom props to components. We may also nest routes and use relative links as well. This helps us prevent or reduce the definition of new routes inside various component files and makes it easy to pass global data to components and props.

When redirecting instead of using useHistory, the new approach using useNavigate has been designed to offer better navigation API. It allows us to set the new route easily via navigate() instead of using multiple methods: history.push(), history.replace(), and history.goBack(). Also, the new v6 API is so minimal and developer friendly. It lets you reduce boilerplate code and complex route definitions to make your app codebase clean.

Finally, the navigation link NavLink allows us to condition active links, which makes our code neater and simpler, instead of passing two different props for an active and inactive state. By doing this, we can easily use a ternary operator to condition which style will be affected when a link is active or not.

Conclusion

After reading this article, I hope you are able to upgrade your React Router version and restructure your codebases to work seamlessly with React Router v6. Updating your code based on the above points helps you to migrate any React Router-based app. But, if your app’s routing is so complex and extensively uses deprecated v5 features, you can consider using the official incremental migration guide from maintainers.

Not only should React Router v5 be upgraded to v6, but Reach Router should be switched to v6 as well.

Regularly upgrade React Router v6 and review the documentation for any changes to stay up-to-date with new releases.