---
title: Upgrading from v6
order: 2
---

# Upgrading from v6

<docs-info>

React Router v7 requires the following minimum versions:

- `node@20`
- `react@18`
- `react-dom@18`

</docs-info>

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**

```shellscript nonumber
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](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#futurev7_relativesplatpath) for more information.

👉 **Enable the flag**

Enabling the flag depends on the type of router:

```tsx
<BrowserRouter
  future={{
    v7_relativeSplatPath: true,
  }}
/>
```

```tsx
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:

```diff
<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:

```diff
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="../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](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#futurev7_starttransition) for more information.

👉 **Enable the flag**

```tsx
<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

<docs-warning>If you are not using a `<RouterProvider>` you can skip this</docs-warning>

**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](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#persistence-future-flag-futurev7_fetcherpersist) for more information.

**Enable the Flag**

```tsx
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

<docs-warning>If you are not using a `<RouterProvider>` you can skip this</docs-warning>

This normalizes `formMethod` fields as uppercase HTTP methods to align with the `fetch()` behavior. [View the CHANGELOG](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#futurev7_normalizeformmethod) for more information.

👉 **Enable the Flag**

```tsx
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**

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

### v7_partialHydration

<docs-warning>If you are not using a `<RouterProvider>` you can skip this</docs-warning>

This enables partial hydration of a data router which is primarily used for SSR frameworks, but it is also useful if you are using `lazy` to load your route modules. It's unlikely you need to worry about this, just turn the flag on. [View the CHANGELOG](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#partial-hydration) for more information.

👉 **Enable the Flag**

```tsx
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`**

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


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

### v7_skipActionErrorRevalidation

<docs-warning>If you are not using a `createBrowserRouter` you can skip this</docs-warning>

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**

```tsx
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**

```js
// 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`**

```js
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.

```diff
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()][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**

```shellscript nonumber
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"`:

```shellscript nonumber
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`:

```diff
-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.

```shellscript nonumber
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:

```shellscript nonumber
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"`:

```diff
-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:

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

Congratulations, you're now on v7!

[react-flushsync]: https://react.dev/reference/react-dom/flushSync
[response-json]: https://developer.mozilla.org/en-US/docs/Web/API/Response/json
[data-util]: https://api.reactrouter.com/v7/functions/react_router.data.html