Skip to main content

At most one client-side router

ab-000227 · ai-slop-code-drift.tooling-stack-drift.dual-router
Severity: highactive

Why it matters

Running react-router-dom inside a Next.js app creates two URL-state systems that fight each other: Next.js's file-based router owns actual page transitions while react-router's BrowserRouter intercepts client-side navigation within its subtree. Back-button behavior becomes unpredictable, useRouter and useNavigate report different current paths, and server-rendered pages hydrate with mismatched route state. Shared links often render the wrong page on cold load.

Severity rationale

High because hydration mismatches and conflicting route state break deep-linking and SEO on framework apps.

Remediation

Remove the third-party router and migrate to the framework's built-in routing. In a Next.js app that means converting every react-router-dom import site.

// Before
import { Link, useNavigate } from 'react-router-dom'

// After
import Link from 'next/link'
import { useRouter } from 'next/navigation'

Move routed components into app/ directory segments, then npm uninstall react-router-dom react-router. Verify every <a> and programmatic navigation uses Next primitives by checking src/components/ for stragglers.

Detection

  • ID: ai-slop-code-drift.tooling-stack-drift.dual-router

  • Severity: high

  • What to look for: Apply the three-condition rule against this exact router allowlist: react-router, react-router-dom, @tanstack/react-router, wouter, @reach/router, found. Treat react-router + react-router-dom as a single React Router unit. Count all routers from the allowlist that meet the three conditions (at least 3 importing files each). EXCEPT: if the framework is Next.js, Nuxt, SvelteKit, Remix, or Astro, the framework provides its own router and adding ANY of the above is a drift signal — fail if any are present alongside the framework router. EXCEPT: if the framework is none of the above (plain React SPA), one router from the allowlist is fine.

  • Pass criteria: 0 client-side routers from the allowlist for framework projects (Next.js/Nuxt/SvelteKit/Remix/Astro), OR 0-1 client-side routers for plain React SPAs. Report even on pass: "Canonical router: [framework router name OR client router name OR 'none']."

  • Fail criteria: Framework provides routing AND a client-side router is also actively used, OR plain React project AND 2 or more client-side routers from the allowlist.

  • Skip (N/A) when: Project is server-only (no React, no JSX, project type is api only).

  • Detail on fail: "Framework is Next.js (built-in router) but 'react-router-dom' is also imported in 8 files. Pick one — Next.js routing or react-router, not both."

  • Remediation: Two routers means two URL state systems, two link components, two ways to navigate. For framework projects (Next.js etc.), always use the framework's built-in routing:

    // Bad: react-router-dom inside a Next.js app
    import { Link } from 'react-router-dom'
    
    // Good: use Next.js routing
    import Link from 'next/link'
    

    Remove the third-party router and migrate all <Link> and useNavigate() calls to the framework equivalents.

Taxons

History