Registry
Aarpit2222 avatar

react-router-v6-to-v7-full

Automated codemod to migrate React Router v6 projects to v7. Handles import rewrites, future flags, RouterProvider fix, json/defer removal, fallbackElement migration, and formMethod casing.

arpit22220 executions

  • upgrade
  • breaking-change
  • react-router
  • v6-to-v7
  • react
Connect a repo
npx codemod react-router-v6-to-v7-full

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)

bash
npx codemod react-router-v6-to-v7-pro

Published Package: Available on codemod registry • Basic migration transforms

🔧 Full Local Version (Advanced)

bash
git 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

RepositoryStarsRR VersionFiles ScannedFiles ModifiedFalse Positives
remix-run/indie-stack~3k^6.x3550
alan2207/bulletproof-react~28k^6.x42520
marmelab/react-admin~25k^6.x1,678540
novuhq/novu~35k^6.x6,4522100
Total8,5902710

✅ 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:

bash
git 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:

  1. Fast early exit — string .includes() check before any AST parsing
  2. AST precision — imports matched by kind: 'import_statement' node, never by line regex; formMethod patterns anchor to navigation.formMethod or fetcher.formMethod specifically
  3. Try/catch on every transform — any unexpected input returns the original source unchanged

Edge Cases Handled

Edge caseOther codemodsThis codemod
RouterProvider import pathreact-router ❌react-router/dom ✅
createBrowserRouter future flags❌✅
export { X } from 'react-router-dom' re-exports❌✅
import type { X } type importsdrops type ❌preserves type ✅
Partial future flags already setduplicates flags ❌adds only missing ✅
json(data, { status: 404 }) two-arg formsilently drops status ❌skipped for manual review ✅
formMethod already uppercasere-transforms ❌idempotent ✅
<Form method={dynamicVar}>changes dynamic value ❌unchanged ✅

Project Structure

text
react-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

Ready to evolve your codebase?

Build tailored, compiler-aware skills to assess, plan, and automate complex codebase changes.