I migrated a full-stack React application from Remix to React Router v7 framework mode over a weekend. The change touched 40 files and removed 591 net lines. Imports and configuration were straightforward; TypeScript took most of the real work.
This part covers why I switched and what tripped me up. Part 2 covers the step-by-step process.
Why React Router v7?
React Router v7 is the upgrade path from Remix v2. That historical consolidation is separate from the later project called Remix v3.
The payoff is immediate. Instead of juggling @remix-run/node, @remix-run/react, @remix-run/serve, and others, shared runtime APIs consolidate into react-router; build tooling still uses @react-router/dev and adapters may need other packages. My dependencies dropped from 16 to 3. The react-router typegen command eliminates manual type annotations in loader functions. The Vite-based build system is cleaner than Remix’s custom setup.
TypeScript Was the Hard Part
The API stayed compatible. TypeScript did not.
The git log tells the story — multiple commits dedicated to resolving type issues:
- LoaderFunctionArgs and useLoaderData typing: Every route needed manual type updates
- Route module type safety: New type patterns required learning React Router v7’s approach
- Interface mismatches: Database query results needed type alignment with component expectations
- Case-sensitivity bugs: The migration exposed case-sensitive database matching in my application; this was not evidence that React Router changed database semantics
The git history ended up with multiple commits devoted only to type errors. Stricter route types were useful once they worked, but they were the migration cost I had underestimated.
What Actually Changed
The migration touched 40 files with 327 insertions and 918 deletions — a net reduction of 591 lines. The main changes: package dependencies (replacing all @remix-run/* packages), build scripts (switched to react-router CLI), and Vite configuration.
The Vite config transformation was dramatic:
- import { vitePlugin as remix } from '@remix-run/dev';
- // Complex remix configuration with future flags
- remix({
- future: {
- v3_fetcherPersist: true,
- v3_relativeSplatPath: true,
- v3_throwAbortReason: true,
- v3_singleFetch: true,
- v3_lazyRouteDiscovery: true,
- }
- })
+ import { reactRouter } from '@react-router/dev/vite';
+ // Simple, clean configuration
+ plugins: [reactRouter(), tsconfigPaths(), tailwindcss()]
Every route and component needed import updates:
- import type { LoaderFunctionArgs } from '@remix-run/node';
- import { useLoaderData } from '@remix-run/react';
+ import type { LoaderFunctionArgs } from 'react-router';
+ import { useLoaderData } from 'react-router';
Loaders, actions, and components work identically. The migration was mostly changing imports and build configuration.
JSX determines the extension Use
.tsxfor files containing JSX. Resource routes without JSX can remain.ts; recognition depends on route configuration.
I simplified my entry.server.tsx during the migration. React Router still supports bot-aware streaming; it does not require removing that split. My build became smaller, but I did not preserve a reproducible before-and-after measurement, so I would not use it to predict another app’s bundle savings.
Continue to Part 2 for the detailed step-by-step migration process and React Router’s typegen feature.
Was this useful?
Thanks for the feedback.