The migration took two focused sessions. Dependency and configuration changes were quick; updating imports and resolving route types across 40 files consumed most of the time.
Part 1 covers why I migrated and the main challenges. This part is the sequence I followed.
The Migration Sequence
Phase 1: Dependencies and Configuration (30 minutes)
Replace all @remix-run/* packages with React Router equivalents in package.json. Change build/dev commands to use the react-router CLI. Simplify Vite configuration. Add react-router.config.ts for framework-specific settings.
Phase 2: Import Refactoring (90 minutes)
This phase ate the most time. Every file importing from @remix-run/* needed updates:
- Route files (23 files): Updated loader/action imports and useLoaderData calls
- Component files (8 files): Changed Link and Form imports
- Entry files (2 files): Updated server/client entry points
- Utility files (7 files): Modified auth and theme utilities
Start with entry points, then routes, then components. Fix TypeScript errors as they appear.
Phase 3: Configuration Fine-tuning (30 minutes)
Updated TypeScript config to use React Router types. Reviewed splat routing: in the standard flat-file convention, api.auth.$.ts is a splat resource route; $rest is a named single-segment parameter, not its replacement. Added the react-router typegen command to dev scripts.
Typegen Changes Everything
Generate the route types
react-router typegenautomatically generates route-specific TypeScript types, eliminating manual interface definitions and preventing type drift.
This feature didn’t exist in Remix. It’s a React Router v7 innovation.
The typegen command generates a +types/<route-file>.d.ts for each route. You get automatic type inference for loader data, route-specific parameter typing, action and loader return type validation, and component prop type safety — all from your actual route implementation.
Remix already supported useLoaderData<typeof loader>. The new feature is generated route-module types, including typed component props and parameters:
import type { Route } from './+types/recipes';
export async function loader({ request }: Route.LoaderArgs) {
return { recipes: await getRecipes(), user: await getUser(request) };
}
export default function RecipeList({ loaderData }: Route.ComponentProps) {
const { recipes, user } = loaderData;
return <pre>{JSON.stringify({ recipes, user }, null, 2)}</pre>;
}
The official Remix upgrade guide describes the dependency and type-generation changes. These types check the contract in TypeScript; they do not validate arbitrary runtime input.
Worth the Switch?
React Router v7 is actively developed. Remix v2 is in maintenance mode. The simplified dependency tree, better TypeScript integration, and improved build system pay off immediately.
For teams on Remix v2 with future flags enabled, the migration is straightforward. AI assistance was critical for batch import updates across 40+ files. What could have been a full day of manual work became two focused sessions.
The application now runs on React Router v7 with fewer dependencies and route types generated from the code. That was enough to justify the weekend.
Was this useful?
Thanks for the feedback.