Skip to main content

Migrating from React Router v6

React Router v7 is a major version upgrade from v6, but the migration path is designed to be incremental. This guide walks you through the process of upgrading your React Router v6 application to v7.

Prerequisites

React Router v7 requires the following minimum versions:
  • Node.js: v20 or higher
  • React: v18 or higher
  • react-dom: v18 or higher

Migration Strategy

The recommended approach is to adopt all future flags in v6 before upgrading to v7. This allows you to update your app one change at a time, committing and shipping each change incrementally.

Step 1: Update to Latest v6.x

First, update to the latest minor version of React Router v6 to access all future flags:
npm install react-router-dom@6

Step 2: Enable Future Flags

Enable each future flag one at a time, test your application, and commit the changes. This incremental approach reduces risk.

v7_relativeSplatPath

What it changes: Updates relative path matching and linking for multi-segment splat paths like dashboard/*. Enable the flag:
// For declarative routing
<BrowserRouter
  future={{
    v7_relativeSplatPath: true,
  }}
/>

// For data routers
createBrowserRouter(routes, {
  future: {
    v7_relativeSplatPath: true,
  },
});
Update your code: If you have routes with multi-segment splat paths like <Route path="dashboard/*">, split them into parent and child routes:
<Routes>
  <Route path="/" element={<Home />} />
-  <Route path="dashboard/*" element={<Dashboard />} />
+  <Route path="dashboard">
+    <Route path="*" element={<Dashboard />} />
+  </Route>
</Routes>
Update relative links to include an extra .. segment:
function Dashboard() {
  return (
    <nav>
-      <Link to="team">Team</Link>
+      <Link to="../team">Team</Link>
    </nav>
  );
}

v7_startTransition

What it changes: Uses React.useTransition instead of React.useState for router state updates. Enable the flag:
<BrowserRouter
  future={{
    v7_startTransition: true,
  }}
/>

// or
<RouterProvider
  future={{
    v7_startTransition: true,
  }}
/>
Update your code: Move any React.lazy calls from inside components to the module scope. React.lazy inside components is incompatible with React.useTransition.

v7_fetcherPersist

Applies to: createBrowserRouter only What it changes: The fetcher lifecycle is now based on when it returns to idle state rather than when its owner component unmounts. Enable the flag:
createBrowserRouter(routes, {
  future: {
    v7_fetcherPersist: true,
  },
});
Update your code: Review any usage of useFetchers - fetchers may persist longer than before. Adjust rendering logic if needed.

v7_normalizeFormMethod

Applies to: createBrowserRouter only What it changes: Normalizes formMethod fields as uppercase HTTP methods. Enable the flag:
createBrowserRouter(routes, {
  future: {
    v7_normalizeFormMethod: true,
  },
});
Update your code: Compare formMethod to UPPERCASE values:
-useNavigation().formMethod === "post"
-useFetcher().formMethod === "get"
+useNavigation().formMethod === "POST"
+useFetcher().formMethod === "GET"

v7_partialHydration

Applies to: createBrowserRouter only What it changes: Enables partial hydration for SSR applications. Enable the flag:
createBrowserRouter(routes, {
  future: {
    v7_partialHydration: true,
  },
});
Update your code: Replace fallbackElement with HydrateFallback on your routes:
const router = createBrowserRouter([
  {
    path: "/",
    Component: Layout,
+    HydrateFallback: Fallback,
    children: [],
  },
]);

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

v7_skipActionErrorRevalidation

Applies to: createBrowserRouter only What it changes: Loaders no longer revalidate by default when an action returns a 4xx/5xx status. Enable the flag:
createBrowserRouter(routes, {
  future: {
    v7_skipActionErrorRevalidation: true,
  },
});
Update your code: If you mutate data in error scenarios, either: Option 1: Refactor to avoid mutations before validation:
// Before
async function action() {
  await mutateSomeData();
  if (detectError()) {
    throw new Response(error, { status: 400 });
  }
}

// After
async function action() {
  if (detectError()) {
    throw new Response(error, { status: 400 });
  }
  await mutateSomeData();
}
Option 2: Opt into revalidation via shouldRevalidate:
function shouldRevalidate({ actionStatus, defaultShouldRevalidate }) {
  if (actionStatus != null && actionStatus >= 400) {
    return true;
  }
  return defaultShouldRevalidate;
}

Step 3: Update Deprecated APIs

Replace json and defer with raw objects:
async function loader() {
-  return json({ data });
+  return { data };
}
If you need JSON serialization, use Response.json():
return Response.json({ data });

Step 4: Upgrade to v7

Once all future flags are enabled and your app works correctly, upgrade to v7:
npm uninstall react-router-dom
npm install react-router@latest
Note: Only react-router is needed in your package.json for v7.

Step 5: Update Imports

Change all imports from react-router-dom to react-router:
-import { useLocation } from "react-router-dom";
+import { useLocation } from "react-router";
Automated script (macOS/BSD sed):
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' {} +
GNU sed (most Linux distributions):
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' {} +

Step 6: Update DOM-Specific Imports

RouterProvider and HydratedRouter now come from a deep import:
-import { RouterProvider } from "react-router-dom";
+import { RouterProvider } from "react-router/dom";
For non-DOM contexts (like Jest tests), use the top-level import:
-import { RouterProvider } from "react-router-dom";
+import { RouterProvider } from "react-router";

Breaking Changes Summary

Package Consolidation

  • All packages collapsed into react-router
  • react-router-dom is maintained for compatibility but re-exports from react-router
  • Runtime-specific exports moved to @react-router/node, @react-router/cloudflare, etc.

Removed APIs

  • defer (use raw promises)
  • json (use raw objects or Response.json())
  • AbortedDeferredError
  • TypedDeferredData
  • UNSAFE_DeferredData
  • Future flags (now default behavior):
    • v7_startTransition
    • v7_relativeSplatPath
    • v7_fetcherPersist
    • v7_normalizeFormMethod
    • v7_partialHydration
    • v7_skipActionErrorRevalidation

Type Changes

  • useFetcher generic now expects the function type (e.g., typeof loader) instead of the data type
  • Update usage: useFetcher<typeof loader>() instead of useFetcher<LoaderData>()

Minimum Version Requirements

  • Node.js: v16 → v20
  • React: Minimum v18 (no change)

Common Issues

If links break after enabling v7_relativeSplatPath, ensure you’ve split multi-segment splat routes and updated relative paths with ../.

Fetcher Lifecycle Changes

With v7_fetcherPersist, fetchers remain in the router state until idle. If you see stale fetchers in useFetchers(), this is expected behavior.

Form Method Casing

After enabling v7_normalizeFormMethod, all formMethod checks must use uppercase: POST, GET, PUT, DELETE, PATCH.

Next Steps

After completing this migration, consider:

Resources

Build docs developers (and LLMs) love