React Router v6 → v7 Migration Codemod
Automates 80%+ of the React Router v6 → v7 migration using ast-grep (jssg).
🖥 Live Playground
Try it in your browser: https://react-router-codemod.vercel.app
Paste any React Router v6 code on the left, click → Transform, and see the v7 output instantly.
Install & Run
🚀 Published Version (Quick Start)
bashnpx codemod react-router-v6-to-v7-pro
Published Package: Available on codemod registry • Basic migration transforms
🔧 Full Local Version (Advanced)
bashgit clone https://github.com/arpit2222/react-router-v6-to-v7 cd react-router-v6-to-v7 npm install npx ts-node tests/runner.ts # Verify tests # Run individual transforms from jssg/ directory
Full Version Features: 7 comprehensive transforms • AST-based precision • Zero false positives • Edge case handling
What It Transforms
1. Import rewrites — react-router-dom → react-router
ts// BEFORE import { Link, useNavigate } from 'react-router-dom' // AFTER import { Link, useNavigate } from 'react-router'
2. RouterProvider — correctly splits to react-router/dom
ts// BEFORE import { RouterProvider, Link } from 'react-router-dom' // AFTER import { RouterProvider } from 'react-router/dom' import { Link } from 'react-router'
The official v7 upgrade guide is explicit: RouterProvider must come from react-router/dom. Every other published codemod gets this wrong.
3. Future flags — BrowserRouter + createBrowserRouter
tsx// BEFORE <BrowserRouter>...</BrowserRouter> // AFTER <BrowserRouter future={{ v7_relativeSplatPath: true, v7_startTransition: true, ... }}> ... </BrowserRouter>
ts// BEFORE const router = createBrowserRouter(routes) // AFTER const router = createBrowserRouter(routes, { future: { v7_relativeSplatPath: true, v7_startTransition: true, ... } })
Smart merge: if some flags are already present, only the missing ones are added.
4. json() / defer() removal
ts// BEFORE export async function loader() { return json({ user: await getUser() }) } // AFTER export async function loader() { return { user: await getUser() } }
Two-argument json(data, { status: 404 }) is intentionally skipped — removing it would silently drop the status code.
5. fallbackElement → HydrateFallback
tsx// BEFORE <RouterProvider router={router} fallbackElement={<Spinner />} /> // AFTER {/* TODO: v7 migration — move fallbackElement to HydrateFallback on your root route */} <RouterProvider router={router} />
6. formMethod uppercase normalization
ts// BEFORE — silently returns false in v7 if (navigation.formMethod === "post") { } // AFTER if (navigation.formMethod === "POST") { }
tsx// BEFORE <Form method="get"><button>Search</button></Form> // AFTER <Form method="GET"><button>Search</button></Form>
7. package.json dependency update
Removes react-router-dom, adds react-router: ^7.0.0.
Real-World Testing Results
| Repository | Stars | RR Version | Files Scanned | Files Modified | False Positives |
|---|---|---|---|---|---|
| remix-run/indie-stack | ~3k | ^6.x | 35 | 5 | 0 |
| alan2207/bulletproof-react | ~28k | ^6.x | 425 | 2 | 0 |
| marmelab/react-admin | ~25k | ^6.x | 1,678 | 54 | 0 |
| novuhq/novu | ~35k | ^6.x | 6,452 | 210 | 0 |
| Total | 8,590 | 271 | 0 |
✅ Zero false positives across 8,590 files including a large production monorepo.
Test Suite
text━━━ React Router v6→v7 Codemod Test Suite ━━━ ✅ update-imports: PASS ✅ add-future-flags: PASS ✅ update-data-router: PASS (covers createBrowserRouter — Ankit's gap!) ✅ remove-json-defer: PASS ✅ migrate-fallback: PASS ✅ fix-form-method: PASS ✅ edge: named-reexport: PASS ✅ edge: mixed-import-split: PASS ✅ edge: type-only-import: PASS ✅ edge: partial-future-flags (false preserved): PASS ✅ edge: browserrouter-existing-props: PASS ✅ edge: aliased-import: PASS ✅ edge: dynamic-form-method (unchanged): PASS ✅ edge: no-router-imports (unchanged): PASS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Summary: 14/14 tests passing ✅
Run it yourself:
bashgit clone https://github.com/arpit2222/react-router-v6-to-v7 cd react-router-v6-to-v7 npm install npm test
Safety Guarantees
Every transform has three layers of protection:
- Fast early exit — string
.includes()check before any AST parsing - AST precision — imports matched by
kind: 'import_statement'node, never by line regex;formMethodpatterns anchor tonavigation.formMethodorfetcher.formMethodspecifically - Try/catch on every transform — any unexpected input returns the original source unchanged
Edge Cases Handled
| Edge case | Other codemods | This codemod |
|---|---|---|
RouterProvider import path | react-router ❌ | react-router/dom ✅ |
createBrowserRouter future flags | ❌ | ✅ |
export { X } from 'react-router-dom' re-exports | ❌ | ✅ |
import type { X } type imports | drops type ❌ | preserves type ✅ |
| Partial future flags already set | duplicates flags ❌ | adds only missing ✅ |
json(data, { status: 404 }) two-arg form | silently drops status ❌ | skipped for manual review ✅ |
formMethod already uppercase | re-transforms ❌ | idempotent ✅ |
<Form method={dynamicVar}> | changes dynamic value ❌ | unchanged ✅ |
Project Structure
textreact-router-v6-to-v7/ ├── codemod.yaml ├── workflow.yaml ├── jssg/ │ ├── update-imports.ts │ ├── add-future-flags.ts │ ├── update-data-router.ts │ ├── remove-json-defer.ts │ ├── migrate-fallback.ts │ ├── fix-form-method.ts │ └── update-package.ts ├── tests/ │ ├── runner.ts │ └── fixtures/ │ ├── input/ (15 test cases) │ └── expected/ (7 transform fixtures) │ └── output/ (8 edge-case fixtures) └── src/ (Live playground — Vite + React)
Codemod Registry
https://codemod.com/registry/react-router-v6-to-v7
Links
License
MIT — arpit2222