Upgrading from v6
On this page

Upgrading from v6

React Router v7 requires the following minimum versions:

  • node@20
  • react@18
  • react-dom@18

The v7 upgrade has no breaking changes if you have enabled all future flags. These flags allow you to update your app one change at a time. We highly recommend you make a commit after each step and ship it instead of doing everything all at once.

Update to latest v6.x

First update to the latest minor version of v6.x to have the latest future flags and console warnings.

👉 Update to latest v6

npm install react-router-dom@6

v7_relativeSplatPath

Background

Changes the relative path matching and linking for multi-segment splats paths like dashboard/* (vs. just *). View the CHANGELOG for more information.

👉 Enable the flag

Enabling the flag depends on the type of router:

<BrowserRouter
  future={{
    v7_relativeSplatPath: true,
  }}
/>
createBrowserRouter(routes, {
  future: {
    v7_relativeSplatPath: true,
  },
});

Update your Code

If you have any routes with a path + a splat like <Route path="dashboard/*"> that have relative links like <Link to="relative"> or <Link to="../relative"> beneath them, you will need to update your code.

👉 Split the <Route> into two

Split any multi-segment splat <Route> into a parent route with the path and a child route with the splat:

<Routes>
  <Route path="/" element={<Home />} />
-  <Route path="dashboard/*" element={<Dashboard />} />
+  <Route path="dashboard">
+    <Route path="*" element={<Dashboard />} />
+  </Route>
</Routes>

// or
createBrowserRouter([
  { path: "/", element: <Home /> },
  {
-    path: "dashboard/*",
-    element: <Dashboard />,
+    path: "dashboard",
+    children: [{ path: "*", element: <Dashboard /> }],
  },
]);

👉 Update relative links

Update any <Link> elements within that route tree to include the extra .. relative segment to continue linking to the same place:

function Dashboard() {
  return (
    <div>
      <h2>Dashboard</h2>
      <nav>
-        <Link to="/">Dashboard Home</Link>
-        <Link to="team">Team</Link>
-        <Link to="projects">Projects</Link>
+        <Link to="../">Dashboard Home</Link>
+        <Link to="../team">Team</Link>
+        <Link to="../projects">Projects</Link>
      </nav>

      <Routes>
        <Route path="/" element={<DashboardHome />} />
        <Route path="team" element={<DashboardTeam />} />
        <Route
          path="projects"
          element={<DashboardProjects />}
        />
      </Routes>
    </div>
  );
}

v7_startTransition

Background

This uses React.useTransition instead of React.useState for Router state updates. View the CHANGELOG for more information.

👉 Enable the flag

<BrowserRouter
  future={{
    v7_startTransition: true,
  }}
/>

// or
<RouterProvider
  future={{
    v7_startTransition: true,
  }}
/>

👉 Update your Code

You don't need to update anything unless you are using React.lazy inside of a component.

Using React.lazy inside of a component is incompatible with React.useTransition (or other code that makes promises inside of components). Move React.lazy to the module scope and stop making promises inside of components. This is not a limitation of React Router but rather incorrect usage of React.

v7_fetcherPersist

If you are not using a <RouterProvider> you can skip this

Background

The fetcher lifecycle is now based on when it returns to an idle state rather than when its owner component unmounts: View the CHANGELOG for more information.

Enable the Flag

createBrowserRouter(routes, {
  future: {
    v7_fetcherPersist: true,
  },
});

Update your Code

It's unlikely to affect your app. You may want to check any usage of useFetchers as they may persist longer than they did before. Depending on what you're doing, you may render something longer than before.

v7_normalizeFormMethod

If you are not using a <RouterProvider> you can skip this

This normalizes formMethod fields as uppercase HTTP methods to align with the fetch() behavior. View the CHANGELOG for more information.

👉 Enable the Flag

createBrowserRouter(routes, {
  future: {
    v7_normalizeFormMethod: true,
  },
});

Update your Code

If any of your code is checking for lowercase HTTP methods, you will need to update it to check for uppercase HTTP methods (or call toLowerCase() on it).

👉 Compare formMethod to UPPERCASE

-useNavigation().formMethod === "post"
-useFetcher().formMethod === "get";
+useNavigation().formMethod === "POST"
+useFetcher().formMethod === "GET";

v7_partialHydration

If you are not using a <RouterProvider> you can skip this

This allows SSR frameworks to provide only partial hydration data. It's unlikely you need to worry about this, just turn the flag on. View the CHANGELOG for more information.

👉 Enable the Flag

createBrowserRouter(routes, {
  future: {
    v7_partialHydration: true,
  },
});

Update your Code

With partial hydration, you need to provide a HydrateFallback component to render during initial hydration. Additionally, if you were using fallbackElement before, you need to remove it as it is now deprecated. In most cases, you will want to reuse the fallbackElement as the HydrateFallback.

👉 Replace fallbackElement with HydrateFallback

const router = createBrowserRouter(
  [
    {
      path: "/",
      Component: Layout,
+      HydrateFallback: Fallback,
      // or
+      hydrateFallbackElement: <Fallback />,
      children: [],
    },
  ],
);


<RouterProvider
  router={router}
-  fallbackElement={<Fallback />}
/>

v7_skipActionErrorRevalidation

If you are not using a createBrowserRouter you can skip this

When this flag is enabled, loaders will no longer revalidate by default after an action throws/returns a Response with a 4xx/5xx status code. You may opt-into revalidation in these scenarios via shouldRevalidate and the actionStatus parameter.

👉 Enable the Flag

createBrowserRouter(routes, {
  future: {
    v7_skipActionErrorRevalidation: true,
  },
});

Update your Code

In most cases, you probably won't have to make changes to your app code. Usually, if an action errors, it's unlikely data was mutated and needs revalidation. If any of your code does mutate data in action error scenarios you have 2 options:

👉 Option 1: Change the action to avoid mutations in error scenarios

// Before
async function action() {
  await mutateSomeData();
  if (detectError()) {
    throw new Response(error, { status: 400 });
  }
  await mutateOtherData();
  // ...
}

// After
async function action() {
  if (detectError()) {
    throw new Response(error, { status: 400 });
  }
  // All data is now mutated after validations
  await mutateSomeData();
  await mutateOtherData();
  // ...
}

👉 Option 2: Opt-into revalidation via shouldRevalidate and actionStatus

async function action() {
  await mutateSomeData();
  if (detectError()) {
    throw new Response(error, { status: 400 });
  }
  await mutateOtherData();
}

async function loader() { ... }

function shouldRevalidate({ actionStatus, defaultShouldRevalidate }) {
  if (actionStatus != null && actionStatus >= 400) {
    // Revalidate this loader when actions return a 4xx/5xx status
    return true;
  }
  return defaultShouldRevalidate;
}

Deprecations

The json and defer methods are deprecated in favor of returning raw objects.

async function loader() {
- return json({ data });
+ return { data };

If you were using json to serialize your data to JSON, you can use the native Response.json() method instead.

Upgrade to v7

Now that your app is caught up, you can simply update to v7 (theoretically!) without issue.

👉 Install v7

npm install react-router-dom@latest

👉 Replace react-router-dom with react-router

In v7 we no longer need "react-router-dom" as the packages have been simplified. You can import everything from "react-router":

npm uninstall react-router-dom
npm install react-router@latest

Note you only need "react-router" in your package.json.

👉 Update imports

Now you should update your imports to use react-router:

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

Instead of manually updating imports, you can use this command. Make sure your git working tree is clean though so you can revert if it doesn't work as expected.

find ./path/to/src \( -name "*.tsx" -o -name "*.ts" -o -name "*.js" -o -name "*.jsx" \) -type f -exec sed -i '' 's|from "react-router-dom"|from "react-router"|g' {} +

If you have GNU sed installed (most Linux distributions), use this command instead:

find ./path/to/src \( -name "*.tsx" -o -name "*.ts" -o -name "*.js" -o -name "*.jsx" \) -type f -exec sed -i 's|from "react-router-dom"|from "react-router"|g' {} +

👉 Update DOM-specific imports

RouterProvider and HydratedRouter come from a deep import because they depend on "react-dom":

-import { RouterProvider } from "react-router-dom";
+import { RouterProvider } from "react-router/dom";

Note you should use a top-level import for non-DOM contexts, such as Jest tests:

-import { RouterProvider } from "react-router-dom";
+import { RouterProvider } from "react-router";

Congratulations, you're now on v7!

Docs and examples CC 4.0