← Back to Blog
React2026-05-05Β·44 min read

I Built a Zero-False-Positive Codemod That Migrates React Router v6 v7 in 3 Seconds

By Ankit raj

I Built a Zero-False-Positive Codemod That Migrates React Router v6 v7 in 3 Seconds

Current Situation Analysis

React Router v7 introduces four simultaneous breaking changes that touch virtually every file in a React application:

❌ react-router-dom β†’ must become react-router
❌ 6 future flags must be added to every Router component  
❌ json() is deprecated β†’ return plain objects
❌ defer() is deprecated β†’ return plain objects

For a typical 50-file codebase, manual migration requires 2+ hours of tedious find-and-replace operations. The failure modes are severe: missing a single import breaks the app, forgetting a flag triggers runtime warnings, and accidentally modifying string literals causes silent corruption.

Traditional text-based approaches (regex/IDE search-replace) fail catastrophically because they lack semantic understanding:

  1. String/Comment Corruption: Regex cannot distinguish between import { Link } from 'react-router-dom' and the same text inside a string literal, comment, or URL.
  2. Complex JSX Injection: Injecting 6 props into a multi-line component with existing props, comments, and nested children requires handling infinite formatting variants. Regex treats this as unstructured text.
  3. Non-Idempotency: Running regex twice produces react-routerr or duplicates future flags. Text matching operates on character sequences, not structural patterns, making safe re-execution impossible.

WOW Moment: Key Findings

Approach Execution Time False Positives Idempotency Semantic Accuracy
Manual/Regex 2+ hours (50 files) High (strings/comments/URLs) Fails on re-run 0% (text-only matching)
AST-Powered Codemod ~3 seconds 0 Guaranteed 100% (tree-structure matching)

Key Findings:

  • AST parsing eliminates semantic ambiguity by matching tree structures instead of character sequences.
  • The smart-merge algorithm for future flags guarantees idempotent execution regardless of partial manual migrations.
  • Real-world validation across react-admin (45 files) and react-petstore (34 files) yielded zero false positives.
  • Sweet Spot: Mid-to-large codebases (30+ files) where manual effort significantly exceeds automation overhead, and strict type/format preservation is required.

Core Solution

The solution leverages ast-grepβ€”a Rust-based AST tool with Node.js bindings. It operates ~100Γ— faster than Babel and supports structural pattern matching natively. Instead of matching text, the engine matches tree structures:

Source:   import { Link, Route } from 'react-router-dom';

AST:      import_statement
          β”œβ”€β”€ named_imports
          β”‚   β”œβ”€β”€ import_specifier ("Link")
          β”‚   └── import_specifier ("Route")
          └── string ("react-router-dom")  ← We match THIS node

The pattern import { $$$IMPORTS } from 'react-router-dom' matches the shape of the AST, ensuring:

  • βœ… Matches regardless of whitespace or formatting
  • βœ… Never matches inside strings or comments
  • βœ… Preserves all import specifiers exactly as written
  • βœ… Preserves inline comments and type annotations

The 4-Step Migration Pipeline

Step 1: Package Migration Reads package.json, replaces react-router-dom with react-router@7, and handles duplicate entries gracefully.

Step 2: Import Rewriting Parses every .ts, .tsx, .js, .jsx file via AST. Finds all import ... from 'react-router-dom' statements and rewrites them to 'react-router'. Formatting, aliases, and comments are all preserved.

Step 3: Future Flag Injection (The Hard Part) A developer might have already added some flags manually. Blindly injecting all 6 would create duplicates. The solution uses a smart-merge algorithm:

const existingFlags = ["v7_startTransition", "v7_relativeSplatPath"];
const allRequired = [
  "v7_relativeSplatPath", "v7_startTransition", 
  "v7_fetcherPersist", "v7_normalizeFormMethod",
  "v7_partialHydration", "v7_skipActionErrorRevalidation"
];

// Only inject what's missing
const missing = allRequired.filter(f => !existingFlags.includes(f));
// β†’ ["v7_fetcherPersist", "v7_normalizeFormMethod", 
//    "v7_partialHydration", "v7_skipActionErrorRevalidation"]

This guarantees idempotent execution β€” run it 10 times, get the exact same output.

Step 4: API Modernization Finds deprecated json() and defer() calls, unwraps them to plain return objects, and cleans up the now-unused imports.

Before & After Transformation

❌ Before (v6):

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { json, defer } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
      </Routes>
    </BrowserRouter>
  );
}

export function loader() {
  return json({ user: getUser() });
}

βœ… After (v7):

import { BrowserRouter, Routes, Route } from 'react-router';

function App() {
  return (
    <BrowserRouter future={{ 
      v7_relativeSplatPath: true,
      v7_startTransition: true,
      v7_fetcherPersist: true,
      v7_normalizeFormMethod: true,
      v7_partialHydration: true,
      v7_skipActionErrorRevalidation: true 
    }}>
      <Routes>
        <Route path="/" element={<Home />} />
      </Routes>
    </BrowserRouter>
  );
}

export function loader() {
  return { user: getUser() };
}

Every import rewritten. Every flag injected. Every deprecated API unwrapped. In under 3 seconds.

Safety First: Backup & Rollback

Before touching a single file, the engine creates a complete snapshot:

  • SHA-256 hash per file for integrity verification
  • Full file-level snapshots in .codemod-backup/
  • One-command rollback: node apply-codemod.js --rollback

If anything goes wrong β€” --rollback restores your entire codebase instantly, verified byte-for-byte against the original hashes.

Pitfall Guide

  1. Regex String/Comment Corruption: Text-based matching blindly replaces react-router-dom inside string literals, comments, and URLs, causing silent runtime failures. Best Practice: Always use AST node targeting (import_statement) to restrict replacements to syntactic import declarations.
  2. Duplicate Future Flags: Blindly injecting all 6 v7 flags into components that already have partial flags creates invalid JSX and duplicate prop errors. Best Practice: Implement a smart-merge algorithm that diffs existingFlags against allRequired and only injects missing entries.
  3. Non-Idempotent Transforms: Running a regex-based codemod twice corrupts package names (react-routerr) and duplicates props. Best Practice: Rely on structural AST matching and conditional injection logic. AST patterns match structural shapes, not character sequences, making re-execution inherently safe.
  4. Ignoring Backup/Rollback Mechanisms: Modifying a codebase without cryptographic verification leaves no recovery path if the transform introduces subtle syntax errors. Best Practice: Pre-compute SHA-256 hashes for all target files, store snapshots in a dedicated .codemod-backup/ directory, and implement a byte-level rollback command.
  5. Overlooking Partial Manual Migrations: Assuming a codebase is 100% legacy v6 leads to conflicts when developers have already manually added flags or updated imports. Best Practice: Design the AST scanner to detect and preserve manually migrated nodes, treating them as idempotent boundaries rather than overwrite targets.
  6. CLI Enum/Variant Parsing Failures: Transformation engines often crash with unresolvable enum errors when step actions don't match expected variants. Best Practice: Validate step actions against a strict schema before execution, and implement graceful fallbacks for unrecognized AST node types.

Deliverables

  • πŸ“˜ AST Migration Blueprint: Complete architecture diagram detailing the 4-step pipeline, AST node traversal logic, and smart-merge flag injection workflow. Includes ast-grep YAML rule configurations for import rewriting, JSX prop injection, and API unwrapping.
  • βœ… Pre/Post-Migration Checklist: Step-by-step validation protocol covering package.json backup, SHA-256 hash generation, dry-run execution, false-positive verification, and rollback testing.
  • βš™οΈ Configuration Templates: Ready-to-use codemod.config.js with environment-aware path resolution, .codemod-backup/ structure template, and the apply-codemod.js rollback script with byte-level integrity verification.