# At most one client-side router

- **Pattern:** `ab-000227` (`ai-slop-code-drift.tooling-stack-drift.dual-router`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000227
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000227)

## 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.

```tsx
// 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:** `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:

  ```tsx
  // 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: dependency-coherence

HTML version: https://auditbuffet.com/patterns/ab-000227
