Migration Guide: Remix → React Router
This guide walks you through migrating from Remix 2.x to the latest version of React Router.
Why migrate?
In May 2024, the Remix team announced that Remix will merge into React Router. This change frees up Remix to focus on a new major version, Remix v3, which introduces significant breaking changes.
In December 2024, React Router 7 was released, bringing essentially all Remix features—and more.
Remix v2 continued to receive updates for a while, but development stopped at version 2.16.8. To keep receiving updates and take advantage of new features (for example, React Server Components), we need to migrate to React Router 7.
Remix vs. React Router: what changes?
In short, React Router provides the same core capabilities as Remix. You can continue using the same patterns, APIs, and components you’re already familiar with.
With React Router 7, there are improvements around type safety (we’ll cover those during the migration) as well as some changes in how loader and action data is accessed and used.
Migration overview
The migration consists of three main steps:
- Dependencies
- Configuration
- Code changes
Don’t worry if your IDE lights up red during the process—that’s expected. You’re switching the underlying framework, so temporary breakage is normal. Once everything is wired up again, the app should behave the same as before. We’ll get it back to green step by step.
Migration with AI
Note: Most importantly you need to understand and be on top of these major changes. It might be beneficial to invest the time doing the steps manual to learn what changed.
You can use AI to assist with parts of the migration, in tiny steps. Steps 1 (Dependencies) and 2 (Configuration) are particularly well-suited for AI automation. We strongly recommend using the GitHub MCP Server to give your coding agent access to the Remix Stack repository.
Step 3 (Code migration) is more challenging for AI due to its complexity and variability. Many files require updates, and implementations differ between projects, which can cause coding agents to struggle with this phase.
Example migrations
1) Dependencies
Remove these dependencies:
@remix-run/react
@remix-run/serve
@remix-run/dev
@remix-run/testing
@pit-shared/remix-esm
flat-routes
Add these dependencies:
pnpm add react-router @react-router/node
pnpm add -D @react-router/dev
2) Configuration
We’ll go through this file by file:
- Add a new file
react-router.config.tsusing the content from: react-router.config.ts. - Update
vite.config.tsto match: vite.config.ts. - Rename
remix.config.mjstostatista.config.tsand rename the imports in the affected files (global search and replace will help you) - Switch to the React Router defaults in
tsconfig.src.json(.react-router/types/**/*, use"module": "preserve"/"moduleResolution": "bundler", and extendrootDirs): tsconfig.src.json. - Update
tsconfig.cdk.json(changes inincludes): tsconfig.cdk.json. - Rename
remix.env.d.tstostatista.d.tsand update its content: statista.d.ts. - In
package.json, replace the oldgenerate-routesWireit target with areact-router-typestarget and ensure it runs before the build
"scripts": {
"react-router-types": "wireit"
},
"wireit": {
"generate-code": {
"dependencies": [
"generate-api",
"icons",
"generate-i18n",
"react-router-types"
]
},
"react-router-types": {
"command": "react-router typegen",
"files": ["statista.config.ts", "**/*.ts", "**/*.tsx"],
"output": [".react-router/types"]
},
"build": {
"command": "react-router build",
"dependencies": ["generate-code"],
"files": [".env", "statista.config.ts", "tailwind.config.ts", "vite.config.ts"]
}
}
- In
package.json, rename the build command from"remix vite:build"to"react-router build". - In
package.json, add"statista.config.ts"to thefileslist for the build command. - Add
"**/.react-router"to theignorearray ineslint.config.js. - Add
.react-routerto.gitignore.
3) Code migration
Update all Remix imports with react-router
All imports from former remix- and internal remix-esm packages need to be converted to imports from the corresponding react-router package. For example:
- ...from "@pit-shared/remix-esm";
+ ...from "react-router";
- ...from "@remix-run/react";
+ ...from "react-router";
Update routePrefixes and basePath imports
Imports pointing to the former Remix config must be updated to the new Statista config.
- import { basePath, routePrefixes } from "../../remix.config.mjs";
+ import { basePath, routePrefixes } from "../../statista.config.js";
A project-wide search and replace will save you time here.
Change routing
In React Router, the default is to define routes in the routes.ts, instead
of file-based routing you know from Remix.
In your application there should exist a routes.ts in /app. If so, delete all
content, if the file does not exist create one.
Then define your routing in that file. Important: You need to prefix
the routes with the basePath of your application.
Example:
import type { RouteConfig } from "@react-router/dev/routes";
import { index, prefix, route } from "@react-router/dev/routes";
import { basePath } from "../statista.config.js";
export default [
...prefix(basePath, [
index("./routes/index.tsx"),
route("some-other-route", "routes/some-other-route.tsx"),]),
] satisfies RouteConfig;
You need to define the routes in the routes.ts following the documentation
of react-router: Routing
Tip: This is an opportunity to rename and restructure the route folder structure,
as you're no longer dependent on e.g. _ or $ as part of the folder names.
Type-safe routing
Type-safe routing has become a part of React Router. Instead of using the self-written
helper route, React Router has href. See docs here: href
- import { route } from `./routes.ts`;
- <Link to={route('/example')}>Click here</Link>
+ import { href } from 'react-router';
+ <Link to={href('/example')}>Click here</Link>
Update i18n handling
- Add
ns: ["common"]to the configuration inapp/i18n.ts. - In
root.tsx, add an import for the new i18n middleware:
import { createI18nMiddleware } from "@pit-shared/remix-runtime/i18n/react-router/middleware";
- In
root.tsx, add the following exports:
const [i18nMiddleware, getI18nInstance, getLocale] = createI18nMiddleware({
config,
hash,
});
export { getI18nInstance, getLocale };
export const middleware: Route.MiddlewareFunction[] = [
i18nMiddleware,
];
- Use i18n in your loader:
export async function loader({ request, context }: Route.LoaderArgs) {
const i18n = getI18nInstance(context);
}
Update entry.server.ts and entry.client.ts
entry.client.ts
- Change the import from
RemixBrowsertoHydratedRouterand replace it
entry.server.ts
Here mainly the i18n usage has changed. If you don't have any custom code in this file we recommend to get the content from entry.server.ts and replace it. Most of it is boilerplate with custom i18n setup code.
Update sso.server.ts
The return-type of optionalAuth and requireAuth changed. If you don't have
any custom changes to this file, replace the content of the file from sso.server.ts.
Otherwise, you're required to do the following changes:
+ import type { data } from "react-router";
+ type WrappedData = ReturnType<typeof data>;
- export async function optionalAuth<T extends Response>(
+ export async function optionalAuth<T extends WrappedData>(
- export async function requireAuth<T extends Response>(
+ export async function requireAuth<T extends WrappedData>(
Due to these changes, the expected return value from optionalAuth and requireAuth
has also changed. Within these two methods, you must wrap the returned object with
data imported from react-router.
Example:
import { data } from 'react-router';
export const loader = () => {
return optionalAuth(request, async () => {
const user = getUser();
if (!user) {
throw Response.json("Page not found", { status: 404 })
}
return data({ userId: user.id })
});
}
Update Tracking
- Create a new file
/app/tracking.tsand add the content of the file tracking.ts - Import
createTrackingMiddlewareintoroot.tsand add this middleware to the middleware export
import { createTrackingMiddleware } from "./tracking";
export const middleware: Route.MiddlewareFunction[] = [
createTrackingMiddleware(),
i18nMiddleware,
];
Migrate loaders and actions (optional, but recommended)
In general there are hooks you know from Remix to retrieve data from loaders
and actions: useLoaderData and useActionData.
With react-router that has changed too, to improve typesafety. React-router creates
a folder .react-router in the root of your application workspace. In that folder
there are types being generated for each route. For type-safety in each route you're
required to import Route and make use of the generated types.
Example:
import type { Route } from "./+types/index.js";
export const loader = ({ request, context }: Route.LoaderArgs) => {
// more code
}
export const action = ({ request, context }: Route.ActionArgs) => {
// more code
}
export default function RouteComponent({ loaderData, actionData }: Route.ComponentProps) {
// more code
}
Tip: These types need to be generated. When running the application the types
are usually generated automatically. When the application is not running you can
force the generation by running pnpm react-router typegen.
- Hooks are being replaced by receiving data via component props
See react-router docs: Data loading
- export default function MyComponent() {
- const { data } = useLoader<typeof loader>();
- }
+ export default function MyComponent({ loaderData }: Route.ComponentProps) {
+ const { data } = loaderData
+ }
- Return objects rather than a
Responsefrom loaders and actions
- return json({foo: 'bar'});
+ return {foo: 'bar'};
- Throw errors and redirects instead of returning them
Since you shouldn't return a response any longer, you need to throw responses, such as redirects or errors.
- return redirect("/some-page");
+ throw redirect("/some-page");
throw new Response("Service not available", { status: 500 });
Migrate tests
Change the import of the stub, everything else should remain the same. But, this depends on how your tests are written.
- import { createRemixStub } from "@remix-run/testing";
+ import { createRoutesStub } from "react-router";
Test and verify
With all changes done you can verify if everything works.
Run pnpm tsc -b - no typescript error should be shown. If there are some, you
should resolve them.
Run pnpm run build - that will build the app, that should be done successfully.
Run pnpm run lint - that will lint the app, warnings and errors should be resolved.
Run pnpm run dev - that will run the application on your local. The app should
be able to start and you should be able to open the app in your browser as before.
Tip: If react-router types are missing run first pnpm react-router typegen.