We try our best to keep major version upgrades simple and boring through the use of opt-in APIs and Future Flags. Future flags are used to gate breaking changes that don't otherwise have a good call-site opt-in strategy. By adopting all opt-in APIs and future flags, you should be able to upgrade to the next major version of React Router with minimal changes.
We plan to ship new major versions roughly once a year as described in our Open Governance Model, so this guide will continue to track future changes you can adopt ahead of the next major release. v9 is currently estimated for mid-2027 when Node 22 reaches EOL.
We highly recommend you make a commit after each step and ship it instead of doing everything all at once. Most flags can be adopted in any order, with exceptions noted below.
React Router v9 will require the following minimum versions (as of now). You can prepare for the upgrade by updating them while still on v8:
node@24+Before adopting any future flags or call-site opt-in changes, you should update to the latest minor version of v8.x to make sure you have access to the latest flags. You may see a number of deprecation warnings as you upgrade, which we'll cover below.
๐ Update to latest v8
npm install react-router@8 @react-router/{dev,node,etc.}@8
No future flags yet
No known planned breaking changes yet
We document some unstable flags here as a reference for folks contributing to the project via beta testing, but they are not generally recommended for production use and may have breaking changes in patch or minor releases - adopt with caution!
future.unstable_enableNodeReadableStreamBackground
Now that the Web Streams API is stable in Node 22+, it's viable for React Router to use React's renderToReadableStream in the server entry.
When no entry.server.tsx file is present, React Router defaults to renderToPipeableStream when a Node runtime is detected, and renderToReadableStream otherwise.
With this flag enabled, React Router will default to renderToReadableStream on all runtimes, including Node. You can continue to use renderToPipeableStream via a custom entry.server.tsx file if needed.
๐ Enable the Flag
import type { Config } from "@react-router/dev/config";
export default {
future: {
unstable_enableNodeReadableStream: true,
},
} satisfies Config;
Update your Code
No code changes are required. If your app has a custom entry.server.tsx, this flag will not change your runtime behavior.
future.unstable_optimizeDepsBackground
This flag lets React Router provide Vite's dependency optimizer with the client entry file and route module files. This can improve dependency optimization in development, but the behavior is still experimental.
๐ Enable the Flag
import type { Config } from "@react-router/dev/config";
export default {
future: {
unstable_optimizeDeps: true,
},
} satisfies Config;
Update your Code
No code changes are required. If you run into dependency optimization issues after enabling this flag, remove the flag and restart the dev server.
future.unstable_routePatternMatchingBackground
This flag opts Data Routers into a new route matcher powered by @remix-run/route-pattern. It supports the existing React Router path syntax and matching behavior, but ranks ambiguous matches by positional specificity instead of aggregate segment scores. This means a route with a longer static prefix can rank above a route with more dynamic segments.
๐ Enable the Flag
import { createBrowserRouter } from "react-router";
const router = createBrowserRouter(routes, {
future: {
unstable_routePatternMatching: true,
},
});
The flag is also available with createHashRouter and createMemoryRouter.
Update your Code
No route configuration changes are required, but you should review any routes with overlapping patterns to ensure the new ranking behavior selects the intended route. This is mostly expected to be an issue when you have deep dynamic param paths which could result in an aggregate score that outweighs a shallower static segment route.
For example, both of these routes match /products/one/two/three:
const routes = [
{ path: "/products/*", id: "products" },
{
path: "/:first/:second/:third/:fourth",
id: "segments",
},
];
The legacy matcher selects segments based on its aggregate segment score. The new matcher selects products because its static products segment is more specific than the dynamic :first segment in the same position.
Once you enable this flag, use the router.match() when you need to match a location (this is currently marked private and will become stable at the same time this flag stabilizes). Standalone matching APIs such as matchRoutes, matchPath, and useMatch continue to use the legacy matcher and may return different matches than the router.
Case-sensitive routes are not currently supported with this flag.