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.
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
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>
);
}
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.
<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.
<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";
<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 />}
/>
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;
}
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.
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!