Migrating Gradually from React Router DOM v5 to v6: A Comprehensive Guide 🚀

javascriptreact-routerreact-router-dommigration

Saturday, February 17, 2024

Have you been using React Router DOM v5 and are considering migrating to v6? This comprehensive guide will walk you through the migration process, step by step, enabling you to upgrade your React applications seamlessly. Let’s dive in! 🏊‍♂️

📖 Overview of React Router DOM v6

Before we begin the migration process, let’s understand the new features and improvements introduced in React Router DOM v6. React Router DOM v6 introduces a hooks-based API, simplified route configuration, better performance optimizations, and a more intuitive syntax for declarative routing. 🌟

🎯 Why Migrate to React Router DOM v6?

React Router DOM is a popular library for handling routing in React applications. With the release of version 6, developers have access to several new features and improvements. In this article, we’ll explore the benefits of migrating to React Router DOM v6 and the considerations developers should keep in mind before making the switch.

😀 Benefits of Migrating to React Router DOM v6:

  • Improved Performance: React Router DOM v6 introduces several performance optimizations, such as lazy loading of routes, code splitting, and asynchronous loading. These features can significantly improve the speed and responsiveness of your application, especially for larger projects.
  • Simplified Route Configuration: The introduction of the <Routes> component in v6 offers a more straightforward and intuitive way to define and handle routes. This new component allows developers to group related routes together and define nested routes more easily.
  • Hooks-based API: React Router DOM v6 introduces a hooks-based API, which allows for better code organization and reusability. Developers can use hooks like useNavigate, useParams, and useLocation to access routing functionality in a more modular and flexible way.
  • Smoother Migration Experience: React Router DOM v6 provides thorough documentation, migration guides, and community support to assist in a successful and seamless migration. The library also includes a compatibility layer that allows developers to use the new v6 API alongside the old v5 API during the migration process.

🙁 Considerations before Migrating:

  • Breaking Changes: React Router DOM v6 introduces several breaking changes that may require modifications to your existing codebase. For example, the <Switch> component has been removed, and developers must use <Routes> instead. Additionally, the syntax for defining routes has changed, and some props have been renamed or removed.
  • Learning Curve: As with any major version upgrade, there may be a learning curve involved in understanding the changes and adapting your code to the new API. Developers should take the time to read the documentation and migration guides carefully and test their code thoroughly before deploying to production.
  • Third-Party Dependencies: Developers should check the compatibility of their existing third-party dependencies with React Router DOM v6. Some dependencies may require updates or have their own migration processes, which can add additional complexity to the migration process.
  • Navigating Outside React Context: While the react-router-dom documentation doesn’t offer a straightforward solution for routing outside of the React context, we have a recommended pattern that can effectively address this issue. In addition, we also provide a solution that may be of assistance to you.

🗺️ Planning the Migration

To ensure a smooth and successful migration, it’s important to plan carefully. Start by assessing how the migration will impact your project, evaluating any potential breaking changes, and creating a backup strategy. It’s also crucial to have a clear understanding of the changes and their implications. In this guide, we offer two paths for migration: gradual migration and Direct migration, which involves upgrading to React Router version 6. Depending on your project, you can choose the path that works best for you. If you’re unsure about the migration cost, opt for gradual migration and monitor the benefits to determine if it’s worth it. You can easily quit if it’s not. However, if you’re confident that the cost is worth it, proceed with the straight migration to version 6. Feel free to choose the path that suits your needs best. We’re here to help you every step of the way! 📋

📈 1. First Path: Gradually Migration

In this migration approach, we use thereact-router-dom-v5-compact package, which has the v6 API. This allows you to run parallel with v5.

  1. Install the package
npm install react-router-dom-v5-compat

2. Render the Compatibility Router

The compatibility package comes with a special <CompatRouter>that synchronizes the state of v5 and v6 APIs, making both APIs available. To use it, simply render the <CompatRouter> directly below your v5 <BrowserRouter>.

import { BrowserRouter } from "react-router-dom";
+ import { CompatRouter } from "react-router-dom-v5-compat";

 export function App() {
   return (
     <BrowserRouter> 
+      <CompatRouter>
         <Switch>
           <Route path="/" exact component={Home} />
           {/* ... */}
         </Switch>
+      </CompatRouter>
     </BrowserRouter>
   );
 }

If you’re curious, this component accesses the history from v5, sets up a listener, and then renders a “controlled” v6 <Router>. This ensures that both v5 and v6 APIs are using the same history instance and working together seamlessly.

3. Render CompatRoute elements inside of Switch

Change <Route> to <CompatRoute>

import { Route } from "react-router-dom";
+ import { CompatRoute } from "react-router-dom-v5-compat";

export function SomComp() {
    return (
      <Switch>
-       <Route path="/project/:id" component={Project} />
+       <CompatRoute path="/project/:id" component={Project} />
      </Switch>
    )
  }

If you’re using CompatRoute, you’ll be happy to know that it’s designed to make both v5 and v6 APIs available to your component tree within the route.

⚠️️ Just keep in mind that it can only be used inside of a Switch, so make sure you’re using it in the right place.

⚠️️ Also, please note that v6 route paths don’t support regular expressions or optional params, but there’s a hook in v6 that can help you meet your use case. If you need to match extra params or regex patterns, just repeat the route with the necessary changes. Hope that helps

- <Route path="/one/:two?" component={Comp} />
+ <CompatRoute path="/one/:two" component={Comp} /> 
+ <CompatRoute path="/one" component={Comp} />

4. Change component code use v6 instead of v5 APIs

This route now has both v5 and v6 routing contexts, which means we can start migrating our component code to v6.

Just keep in mind that if you’re working with a class component, you’ll need to convert it to a function component first in order to use hooks. But don’t worry, it’s a quick and easy process, and we’re here to help if you need any guidance. Let’s get started on this exciting new chapter! 💪

👉 Read from v6 useParams() instead of v5 props.match

+ import { useParams } from "react-router-dom-v5-compat";

function Project(props) {
-    const { params } = props.match;
+    const params = useParams();
     // ...
  }

👉 Read from v6 useLocation() instead of v5 props.location

+ import { useLocation } from "react-router-dom-v5-compat";

function Project(props) {
-    const location = props.location;
+    const location = useLocation();
     // ...
  }

👉 Use navigate instead of history

+ import { useNavigate } from "react-router-dom-v5-compat";

function Project(props) {
-    const history = props.history;
+    const navigate = useNavigate();
     return (
       <div>
         <MenuList>
           <MenuItem onClick={() => {
-            history.push("/elsewhere");
+            navigate("/elsewhere");
-            history.replace("/elsewhere");
+            navigate("/elsewhere", { replace: true });
-            history.go(-1);
+            navigate(-1);
           }} />
         </MenuList>
       </div>
     )
  }

Great news! 🎉 This component is now utilizing both APIs simultaneously, which means that every small change can be committed and shipped without the need for a long-running branch that makes you want to quit your job, No need for a never-ending branch that makes you feel like you’re stranded on a deserted island, surviving only on coconuts and fish. 🌴🐟 So let’s celebrate this milestone and keep up the great work! 💪

5. (Maybe) Update Links and NavLinks

With React Router v6, you don’t have to manually build the path for deeper URLs when linking to them from match.url. This is because relative links are now supported, making it easier to link to deeper pages without knowing the entire URL beforehand.

👉 Update links to use relative to values

- import { Link } from "react-router-dom";
+ import { Link } from "react-router-dom-v5-compat";

function Project(props) {
     return (
       <div>
-        <Link to={`${props.match.url}/edit`} />
+        <Link to="edit" />
       </div>
     )
  }

The way to define active className and style props has been simplified to a callback to avoid specificity issues with CSS:

👉 Update nav links

- import { NavLink } from "react-router-dom";
+ import { NavLink } from "react-router-dom-v5-compat";

function Project(props) {
     return (
       <div>
-        <NavLink exact to="/dashboard" />
+        <NavLink end to="/dashboard" />
-        <NavLink activeClassName="blue" className="red" />
+        <NavLink className={({ isActive }) => isActive ? "blue" : "red" } />
-        <NavLink activeStyle={{ color: "blue" }} style={{ color: "red" }} />
+        <NavLink style={({ isActive }) => ({ color: isActive ? "blue" : "red" }) />
       </div>
     )
  }

6. Convert Switch to Routes

Once every descendant component in a <Switch> has been migrated to v6, you can convert the <Switch> to <Routes> and change the <CompatRoute> elements to v6 <Route> elements.

👉 Convert <Switch> to <Routes> and <CompatRoute> to v6 <Route>

import { Routes, Route } from "react-router-dom-v5-compat";
- import { Switch, Route } from "react-router-dom"

- <Switch>
-  <CompatRoute path="/" exact component={Home} />
-  <CompatRoute path="/projects/:projectId" component={Project} />
- </Switch>
+ <Routes>
+   <Route path="/" element={<Home />} />
+   <Route path="projects/:projectId" element={<Project />} />
+ </Routes>

7. Rinse and Repeat up the tree

Once you’ve converted the deepest Switchcomponents, move up to their parent <Switch> and repeat the process.

Keep doing this until all components are migrated to v6 APIs. If you’re converting a <Switch> to <Routes> that has descendant <Routes> deeper in its tree, there are a couple of things you need to do in both places to ensure everything continues to match correctly.

👉️ Add splat paths to any <Route> with a descendant <Routes>

function Root() {
    return (
      <Routes>
-       <Route path="/projects" element={<Projects />} />
+       <Route path="/projects/*" element={<Projects />} />
      </Routes>
    );
  }

This ensures deeper URLs like /projects/123 continue to match that route. Note that this isn't needed if the route doesn't have any descendant <Routes>.

👉 Convert route paths from absolute to relative paths

- function Projects(props) {
-   let { match } = props
  function Projects() {
    return (
      <div>
        <h1>Projects</h1>
        <Routes>
-         <Route path={match.path + "/activity"} element={<ProjectsActivity />} />
-         <Route path={match.path + "/:projectId"} element={<Project />} />
-         <Route path={match.path + "/:projectId/edit"} element={<EditProject />} />
+         <Route path="activity" element={<ProjectsActivity />} />
+         <Route path=":projectId" element={<Project />} />
+         <Route path=":projectId/edit" element={<EditProject />} />
        </Routes>
      </div>
    );
  }

Previously, descendant Switch components (now Routes) used the ancestor match.path to build their entire path. However, when the ancestor Switch is converted to <Routes>, this step is no longer necessary as it happens automatically. Just remember to change them to relative paths, otherwise they won’t match.

8. Remove the compatibility package!

After converting all of your code, you can remove the compatibility package and directly install React Router DOM v6. To complete the process, there are a few things we need to do all at once.

👉 Remove the compatibility package

npm uninstall react-router-dom-v5-compat

👉 Uninstall react-router and history

v6 no longer requires history or react-router to be peer dependencies (they’re normal dependencies now), so you’ll need to uninstall them

npm uninstall react-router history

⚠️️ Just a quick heads up — if you remove the history package, you won’t be able to use it for navigation outside of the React context. However, don’t worry! We’ve got you covered. In the ‘Navigating Outside React Context in React Router DOM v6’ section, we discuss alternative approaches you can use instead.

👉 Install React Router v6

npm install react-router-dom@6

👉 Remove the CompatRouter

import { BrowserRouter } from "react-router-dom";
- import { CompatRouter } from "react-router-dom-v5-compat";

export function App() {
    return (
      <BrowserRouter>
-       <CompatRouter>
        <Routes>
          <Route path="/" element={<Home />} />
          {/* ... */}
        </Routes>
-       </CompatRouter>
      </BrowserRouter>
    );
  }

⚠️️ Note that BrowserRouter is now the v6 browser router.

👉 Change all compat imports to “react-router-dom”

You should be able to a find/replace across the project to change all instances of “react-router-dom-v5-compat” to “react-router-dom”

🔍 Additional Resources

👣 2. Second Path: Direct Migration Process

1. Update Package Dependencies

Begin by updating the React Router DOM package to the latest version. In your project directory, run the following command:

npm install react-router-dom@next

2. Adjust Route Configurations

React Router DOM v6 introduces a new <Routes> component for defining routes. Replace your existing <Switch> component with <Routes>, and adjust your individual routes accordingly. For example:

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

ReactDOM.render(
  <Router>
    <Switch>
      <Route path="/" component={Home} />
      <Route path="/about" component={About} />
      <Route path="/contact" component={Contact} />
      <Route component={NotFound} />
    </Switch>
  </Router>,
  document.getElementById('root')
);


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

ReactDOM.render(
  <Router>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/contact" element={<Contact />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  </Router>,
  document.getElementById('root')
);

3. Update Route Components

In v6, the component attribute of the <Route> component has been replaced with the element attribute. Update your route components accordingly. For example:

// Before
<Route path="/about" component={About} />

// After
<Route path="/about" element={<About />} />

4. Handle Nested Routes

React Router DOM v6 introduces nested routes using the useRoutes hook. Update your nested routes by extracting them into separate components, utilizing the useRoutes hook. For example:

// Before
<Route path="/posts" component={Posts}>
  <Route path="/posts/:id" component={Post} />
</Route>

// After
import { useRoutes } from 'react-router-dom';
function Posts() {
  const routes = useRoutes([
    { path: '/', element: <PostsList /> },
    { path: '/:id', element: <PostDetail /> }
  ]);
  return routes;
}

✨ Navigating Outside React Context in React Router DOM v6

In previous versions of React Router, it was possible to navigate outside of the React context by utilizing packages like history. However, in React Router DOM v6, there is no direct equivalent to the history package, and the recommended approach may introduce certain complexities. Let's explore the available options and their limitations.

⚠️ Note: The method outlined in this section involves an approach that is considered unstable and is not recommended in the official documentation. Use it with caution and consider alternative solutions if possible.

  1. Usage of createBrowserRouter and Its Limitations

In some discussions and GitHub issues, there has been mention of using the createBrowserRouter function as a workaround. This function returns a router object that can be used for navigation operations. However, it's important to note that this method is not officially recommended and can lead to circular dependencies if used within components that are already part of the React Router context.

Here’s an example of how createBrowserRouter can be used:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { createBrowserRouter, RouterProvider } from "react-router-dom"

const router = createBrowserRouter([
  // match everything with "*"
  { path: "*", element: <App /> }
])

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
)

And then the rest of your routes will work as they always did inside of components:

import { Routes, Route, Link } from "react-router-dom";

export default function App() {
  return (
    <main>
      <h1>Can use descendant Routes as before</h1>
      <ul>
        <li>
          <Link to="about">About</Link>
        </li>
      </ul>
      <Routes>
        <Route index element={<div>Index</div>} />
        <Route path="about" element={<div>About Page</div>} />
        <Route path="contact" element={<div>Contact Page</div>} />
      </Routes>
    </main>
  )
}

The above code demonstrates the usage of createBrowserRouter. However, please note that this approach is considered unstable and can result in a reference cycle issue if you use the router inside the tree or the function that uses the router Call in the tree.

2. Use the Event Emitter Pattern (What We Recommend!) 📢

Given the limitations and risks associated with the aforementioned approach, it is advisable to consider alternative solutions for navigating outside of the React context in React Router DOM v6. One possible solution is to leverage an event emitter, similar to an observer pattern implemented using libraries like eventemitter3. But you can write your own simple emitter class in emitter.js:

import type { NavigateOptions, To } from 'react-router-dom';

class Emitter<T> {
  private events = new Map<string, Array<Function>>();

  on(type: string, listener: Function) {
    this.events.set(type, this.events.get(type) || []);
    const listeners = this.events.get(type)!;
    listeners.push(listener);
  }

  emit(type: string, ...args: T[]) {
    const listeners = this.events.get(type);
    if (listeners) {
      listeners.forEach((listener: Function) => {
        listener(...args);
      });
    }
  }

  off(type: string, listener: Function) {
    const listeners = this.events.get(type);
    if (listeners) {
      const index = listeners.indexOf(listener);
      if (index > -1) {
        listeners.splice(index, 1);
      }
    }
  }
}

export default Emitter;

// routes emitter
export const routesEmitter = new Emitter<{
  to: To;
  options?: NavigateOptions;
}>();

In the above code, we create a new instance of Emitter and export it as routesEmitter. This emitter will be used to propagate navigation events.

  1. Create a Routes Event Component
import { useEffect } from 'react';
import type { Location, NavigateOptions, To } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { routesEmitter } from 'your-path/emitter';

// constants
export const ROUTE_EVENT_NAVIGATE = 'navigate';
export const ROUTE_EVENT_LISTEN = 'listen';

const RoutesEvent = () => {
  const navigate = useNavigate();
  const location = useLocation();

  useEffect(() => {
    const routeChangeHandler = (callback: (location: Location) => void) => {
      callback(location);
    };

    routesEmitter.on(ROUTE_EVENT_LISTEN, routeChangeHandler);

    return () => {
      routesEmitter.off(ROUTE_EVENT_LISTEN, routeChangeHandler);
    };
  }, [location]);

  useEffect(() => {
    const navigationHandler = ({
      to,
      options,
    }: {
      to: To;
      options?: NavigateOptions;
    }) => {
      navigate(to, options);
    };

    routesEmitter.on(ROUTE_EVENT_NAVIGATE, navigationHandler);

    return () => {
      routesEmitter.off(ROUTE_EVENT_NAVIGATE, navigationHandler);
    };
  }, []);

  return null;
};

export default RoutesEvent;

In the code above, we use the created component to listen to the events of the route. Now we should place this inside our route. It should look like this:

import React from 'react';
import {
  BrowserRouter as Router,
} from 'react-router-dom';
import RoutesEvent from 'your-path/routes-event';    

<Router>
   <RoutesEvent />
   {/* routes...  */}
</Router>

now our RoutesEvnet component listen to the navigate , listien events.

2. Create routesEvent Services (Optional!) 🛠️

For a cleaner way to use the emitter outside the React context, we can write objects to use for navigation, just like normal navigation:

import type { NavigateOptions, To } from 'react-router-dom';
import {
  ROUTE_EVENT_LISTEN,
  ROUTE_EVENT_NAVIGATE,
} from 'your-path/routes-event';
import { routesEmitter } from 'your-path/emitter';

type RoutesEvent = {
  navigate: (to: To, options?: NavigateOptions) => void;
  listen: (callback: any) => void;
};

export const routesEvent: RoutesEvent = {
  navigate: (to, options) =>
    routesEmitter.emit(ROUTE_EVENT_NAVIGATE, { to, options }),
  listen: (callback) => routesEmitter.emit(ROUTE_EVENT_LISTEN, callback),
};

In this way, we have a clear way of navigation.

3. Usage 🚀
Now you can use the routesEvent object to navigate or listen outside the React context:

import { routesEvent } from 'your-path/routes-event';

// navigate
routesEvent.navigate("your-path");

//replace
routesEvent.navigate("your-path", { replace: true });

// with-options
routesEvent.navigate("your-path", {
  replace: true,
  state: { test: true },
});

// listen
routesEvent.listen((routeDetails) => {
  // your logic
});

⚠️ Caution: Navigating outside the React context using an event emitter may introduce additional complexities and potential maintenance issues, especially if not implemented carefully. Consider using this approach only if there are no alternative solutions suited to your specific use case.

Now you can navigate outside the React context with this pattern. It is nice to tell that you can use Redux for this and dispatch some keys and listen to it and navigate, but it’s overkill because for this simple job we don’t need to store data in Redux.

Also, there is a very simple method which is using the browser’s own location API. Depending on your needs, you can choose either approach.

📍 Use Location Window API

window.location.href = 'your-path';

As you can see, we achieved our goal very easily and without any hassle or trouble. Now you might say, “You gave all these explanations, and it was solved with just one line.” 🤣
But with this method, your app’s navigations are no longer fully controlled by react-router-dom, and you may encounter strange and unexpected issues. However, this is a simple approach that could be a suitable solution depending on the project.

🔍 Additional Resources

For further details, discussions, and potential alternatives regarding navigating outside the React context in React Router DOM v6, you can refer to the following GitHub issue:

GitHub issue — Navigate Outside React Context

😲 Paths are Exact By default

In React Router v6, the paths are exact by default, meaning they must match exactly for a route to be rendered. According to the React Router v6 documentation, if you want a route to be non-exact, you can add /* it to the end of the path. This allows the route to match any additional characters after the specified path.

Here’s an example to illustrate the usage of non-exact paths in React Router v6:

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

function App() {
  return (
    <Router>
      <Route exact path="/" component={Home} />
      <Route exact path="/login" component={Login} />
      <Route exact path="/dashboard" component={Dashboard} />
      <Route component={NotFound} /> {/* This will catch 404 */}
    </Router>
  );
}

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

function App() {
  return (
    <Router>
      <Route path="/" element={<Home />} />
      <Route path="/login" element={<Login />} />
        {/* optional /*}
      <Route path="/dashboard/*" element={<Dashboard />} />
    </Router>
  );
}

In this example, the paths /, /loginare exact. So if a user mistypes the URL like /loginsss, it will be caught by the catch-all route { path: '*', element: <NotFound /> } and render the <NotFound /> component.

You can add middleware and perform redirections within the <NotFound /> component based on your business logic. For example, you can redirect unauthorized users to the login page or redirect authenticated users to the home page.

🛠️ Handling Breaking Changes

React Router DOM v6 introduced some breaking changes, such as changes to the Route and Link components. See the official documentation for a complete list of breaking changes and their recommended migrations. 🚧

🧪 Testing and Debugging

Perform comprehensive testing before and after the migration. Update your existing tests to account for the changes in React Router DOM v6. Utilize tools like React Testing Library or Jest to ensure your application functions as expected. 🧪

⚡ Optimizing Performance

Take advantage of the performance improvements offered by react-router-domv6. Leverage lazy loading of routes using dynamic imports, code splitting, and asynchronous loading. Optimize your navigation and rendering processes to boost overall performance. 🚀

✨ Best Practices and Tips

  • Follow the official migration guide and documentation provided by React Router.
  • Ensure your development environment is up to date with the latest versions of React and React Router DOM.
  • Commit your changes incrementally, ensuring that each step is fully functional before proceeding to the next.
  • Engage with the community for guidance and assistance through forums, Stack Overflow, or React Router’s GitHub repository. 🤝

🔍 Additional Resources

🎉 Conclusion

Congratulations! 🎉 You’ve done it! You’ve successfully migrated your React application from React Router DOM v5 to v6. By following this comprehensive guide, you’ve ensured a smooth and efficient transition, enabling your app to benefit from the enhanced features and improved performance of v6.

Remember, migration processes can be complex, so it’s important to take your time, test thoroughly, and seek help from the vibrant React community if needed. We’re here to support you every step of the way, so don’t hesitate to reach out if you need any assistance. Good luck with your migration journey, and happy coding! 💻