# react-router correction pack · for projects on react-router@^7.12 || ^8
<!--
     Stale Priors Index · Unattended Works · https://github.com/SamAndrzejewski

     GENERATED FILE — DO NOT EDIT BY HAND. Your edit will be destroyed by the next build.
     Rebuild:  node tools/build-corrections.mjs
     Sources:  data/react-router/facts.json  (the corrections, each verified against a primary source)
               data/react-router/*.json      (the evidence: reproduced model failures)

     Latest react-router: 8.3.1 · verified 2026-09-09
     Coverage:  — 

     Paste into CLAUDE.md / AGENTS.md / .cursorrules if your project uses react-router@^7.12 || ^8.
     Do NOT use if you are pinned to react-router@6.x, and the separate `react-router-dom` package, which has no v8 release (LF5).
-->

## What we actually measured

0 Claude models were asked for idiomatic react-router code with no tools, purely from training knowledge. 0 reproduced failures across 0 runs, each verified against the release that broke the belief.

## How to read an entry

Every entry ends with a *Reproduced against* line. Where it names models, we have the generated code that got it wrong, dated, with the model's own words in the run write-up. Where it says no model yet, the correction is verified from the release notes but nothing has been probed for it — it is a fix, not a measurement, and the pack says so rather than blurring the two.

The section an entry sits in is the worst case if you act on the stale belief. The severity in brackets after a model's name is what that particular model's output actually did, which can be milder — a model can hold the wrong belief and still, on the day, write code that runs.

## The corrections

### Breaks the build, or throws at runtime

Act on the stale belief here and the code does not run. Fix these first.

#### `react-router-dom`

**Removed in react-router 8.0.0** (2026-06-17)

The `react-router-dom` package is not published on the v8 line — 8.0.0 removed it. Its npm `latest` tag still points at **7.18.3**, while `react-router`'s `latest` is on the 8.x line, so `npm install react-router-dom` in a v8 project installs a v7 router beside your v8 one and leaves two major lines of React Router in the same tree. On v8 there is no DOM package to install: import `RouterProvider` and `HydratedRouter` from `react-router/dom`, and everything else from `react-router`. In v7 this was already the real layout — `react-router-dom` was kept only as a re-export so v6 imports would keep resolving.

*The stale belief:* That a browser React Router app installs and imports from `react-router-dom`, which is where `BrowserRouter`, `RouterProvider`, `Link` and the hooks live.

```ts
// Stale
// v8: react-router-dom has no 8.x release, so this resolves to 7.18.3
import { RouterProvider, Link, useNavigate } from 'react-router-dom'

// Current
// v8
import { RouterProvider } from 'react-router/dom'
import { Link, useNavigate } from 'react-router'
```

> MEASURED AGAINST THE REGISTRY 2026-09-09, which is what makes the claim checkable rather than a reading of the release note: `react-router-dom`'s dist-tags are `latest: 7.18.3` (published 2026-08-28) with no 8.x version present, while `react-router`'s `latest` is 8.3.1. So the package was not unpublished and is not dead - it is still being patched on the 7.x line - it simply does not follow react-router to v8, which is the shape a model is most likely to get wrong: the import does not fail to resolve, it resolves to the wrong major. NOT BISECTABLE by `tools/audit/bisect-facts.mjs`, which installs one release of ONE package per rung; this fact is about a second package's dist-tags, so it carries no `measured_range`.

*Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry.*

Source: [react-router CHANGELOG.md — 8.0.0 major changes](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) · 2026-06-17 · [react-router CHANGELOG.md — 8.0.0 major changes](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) · 2026-06-17 · [npm registry metadata for react-router-dom](https://registry.npmjs.org/react-router-dom)

#### react-router@8 engine and peer requirements

**New requirement in react-router 8.0.0** (2026-06-17) · boundary measured on 21 releases, react-router 7.9.0 through 8.3.1

react-router 8 raised both floors: `engines.node` is `>=22.22.0` and the `react` / `react-dom` peer range is `>=19.2.7`. On the 7.x line they are `>=20.0.0` and `>=18`. A project on Node 20 or React 18 that installs `react-router@latest` gets a peer-dependency failure from npm (or a silently unmet peer under `--legacy-peer-deps`), not a runtime deprecation warning. Pin `react-router@^7` if either floor is a problem; the 7.x line is still published and patched.

*The stale belief:* That react-router runs on Node 20 and React 18, which was true for the whole 7.x line.

```jsonc
// Stale
{
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router": "latest"
  }
}

// Current
{
  "dependencies": {
    "react": "^19.2.7",
    "react-dom": "^19.2.7",
    "react-router": "^8.0.0"
  }
}
// or stay on the maintained 7.x line:
// "react-router": "^7.18.0"   // Node >=20, React >=18
```

> READ OFF THE INSTALLED MANIFESTS 2026-09-09, not off the release note: `react-router@7.11.0` declares `engines {"node":">=20.0.0"}` and peers `react >=18` / `react-dom >=18`; `react-router@8.3.1` declares `engines {"node":">=22.22.0"}` and peers `react >=19.2.7` / `react-dom >=19.2.7`. BISECTED 2026-09-09 over a 21-rung ladder, 7.9.0 to 8.3.1, one release per directory resolved alone (JOURNAL/108). Both floors move at 8.0.0 and neither moves before it: 7.18.3, the last release on the 7.x line and the rung immediately below the major, still declares `>=20.0.0` and `>=18`. The two halves were probed separately so that a release moving one floor without the other would report rather than confirm. The same 8.0.0 entry also announces the published packages becoming ESM-only. THAT HALF IS DELIBERATELY NOT WRITTEN AS A FACT, BECAUSE IT WAS MEASURED AND DOES NOT PRODUCE THE FAILURE IT SOUNDS LIKE: `require("react-router")` **succeeds** at 8.3.1 on Node 24.13.0, returning all 129 exports, because Node >= 22 supports `require()` of an ES module - and the new engine floor of 22.22.0 guarantees every supported Node can do it. A pack entry saying "require() now throws" would have been wrong on the shipped artifact.

*Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry.*

Source: [react-router CHANGELOG.md — 8.0.0 major changes](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) · 2026-06-17 · [react-router CHANGELOG.md — 8.0.0 major changes](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) · 2026-06-17 · [npm registry metadata for react-router](https://registry.npmjs.org/react-router)

### Runs, but is silently wrong

Nothing errors. The behaviour is simply not what a model trained earlier will tell you.

#### pattern (loader / action argument)

**Renamed in react-router 7.15.0** (2026-05-05) · boundary measured on 21 releases, react-router 7.9.0 through 8.3.1

The instrumentation APIs lost their `unstable_` prefix in react-router 7.15.0. The route pattern handed to a `loader`/`action` is `args.pattern`, not `args.unstable_pattern`, and the router option is `instrumentations`, not `unstable_instrumentations`; the `unstable_ServerInstrumentation`, `unstable_ClientInstrumentation`, `unstable_InstrumentRequestHandlerFunction`, `unstable_InstrumentRouterFunction`, `unstable_InstrumentRouteFunction` and `unstable_InstrumentationHandlerResult` types dropped the prefix too. The old names are **gone, not aliased**: reading `args.unstable_pattern` on 7.15.0 and above yields `undefined` in JavaScript rather than a type error, so a telemetry span or log line built from it is labelled `undefined`.

*The stale belief:* That the route pattern inside a loader is `args.unstable_pattern` and the router option is `unstable_instrumentations`, because the instrumentation API is still unstable.

```ts
// Stale
export async function loader(args: Route.LoaderArgs) {
  // undefined on 7.15.0 and above - the span name silently becomes 'undefined'
  span.setAttribute('route', args.unstable_pattern)
}

// Current
export async function loader({ pattern }: Route.LoaderArgs) {
  // 7.15.0 and above
  span.setAttribute('route', pattern)
}
```

> EXECUTED 2026-09-09 against the shipped packages: the loader argument keys are `[context, params, unstable_pattern, request]` at `react-router@7.11.0` and `[context, params, pattern, request, url]` at `8.3.1` — so the rename is measured in both directions, the old key absent above and the new key absent below. BISECTED 2026-09-09 over a 21-rung ladder, 7.9.0 to 8.3.1, one release per directory resolved alone (JOURNAL/108). The changelog's 7.15.0 is CONFIRMED and every clause of the statement was probed separately rather than the headline: the new argument key arrives at 7.15.0 (absent at 7.14.2); the old key is GONE and not aliased there; all six renamed TYPES are present unprefixed in the shipped `dist/` at 7.15.0 and every `unstable_` spelling of them absent, both directions checked, so the type clause is read off the published declarations rather than left as a citation; and the router OPTION `unstable_instrumentations`, which is a different identifier from the argument and would otherwise have ridden on it, is likewise gone at 7.15.0. FOUND BY THE LADDER AND NOT CLAIMED BY THIS FACT: `unstable_pattern` itself arrived at 7.9.5, a PATCH release, so it exists on exactly 7.9.5 through 7.14.2. That matters for how the row is scored rather than for what the fact says - probed as `the old key is absent` it is also true below 7.9.5, where there was no such key to remove, and a bisect reads that as a boundary at the floor.

*Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry.*

Source: [react-router CHANGELOG.md — 7.15.0](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) · 2026-05-05

#### url (loader / action / middleware argument)

**Added in react-router 7.15.0** (2026-05-05) · boundary measured on 21 releases, react-router 7.9.0 through 8.3.1

`loader`, `action` and `middleware` receive a `url` argument since react-router 7.15.0, where it was stabilised from `unstable_url`. It is the **normalised** URL: React Router's own transport details — the `.data` suffix on client data requests and the `index` / `_routes` search params — are already stripped from it. Read `args.url` rather than `new URL(args.request.url)` when you want the URL the user is on. This matters most on the v8 line, where the `future.v8_passThroughRequests` flag was removed and the raw incoming `request` is now always what your loader gets, so `request.url` can carry those suffixes.

*The stale belief:* That the only URL available inside a loader or action is `request.url`, and that parsing it with `new URL(...)` yields the route's own pathname.

```ts
// Stale
export async function loader({ request }: Route.LoaderArgs) {
  // On a client data request this pathname can be '/books/7.data',
  // and the search params can carry React Router's own `index` key.
  const { pathname, searchParams } = new URL(request.url)
  return load(pathname, searchParams)
}

// Current
export async function loader({ url }: Route.LoaderArgs) {
  // 7.15.0 and above: already normalised - no .data suffix, no `index` param
  return load(url.pathname, url.searchParams)
}
```

> TWO HALVES, ONE EXECUTED. The **presence** of `url` was executed 2026-09-09 against the shipped packages: a `createStaticHandler` loader driven through `query()` sees argument keys `[context, params, unstable_pattern, request]` at `react-router@7.11.0` and `[context, params, pattern, request, url]` at `8.3.1`, in plain Node with no DOM. The second half — that `request.url` carries the `.data` suffix on the v8 line — is taken from the 8.0.0 changelog entry removing `future.v8_passThroughRequests` and has NOT been executed here; it needs a framework-mode server request, which `createStaticHandler` does not produce. BISECTED 2026-09-09 over a 21-rung ladder, 7.9.0 to 8.3.1, one release per directory resolved alone (JOURNAL/108). The changelog's 7.15.0 is CONFIRMED for the presence half: no `url` argument at 7.14.2, the release immediately below it, and a real `URL` whose pathname is the route's at 7.15.0 and above. THE `stabilised from unstable_url` CLAUSE IS NOW MEASURED RATHER THAN QUOTED, and it moves nothing in this fact while adding a date the changelog does not give: `unstable_url` exists on exactly 7.13.2 through 7.14.2 - absent at 7.13.1, absent from 7.15.0 - so the unstable spelling arrived in a PATCH release and lasted three releases. The second half of the statement, that `request.url` carries the `.data` suffix on the v8 line, is still unexecuted for the reason above: it needs a framework-mode server request that `createStaticHandler` does not produce.

*Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry.*

Source: [react-router CHANGELOG.md — 7.15.0](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) · 2026-05-05 · [react-router CHANGELOG.md — 8.0.0](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) · 2026-06-17

#### `generatePath`

**Behaviour changed in react-router 7.12.0** (2026-01-07) · boundary measured on 21 releases, react-router 7.9.0 through 8.3.1

`generatePath` substitutes a dynamic segment that is followed by a literal suffix since react-router 7.12.0. `generatePath("/books/:id.json", { id: "7" })` returns `"/books/7.json"`. On 7.11.0 and below the same call returns the pattern **unchanged** — `"/books/:id.json"` — with no error, no warning and no missing-parameter message, so the URL is silently wrong and a fetch against it 404s at runtime rather than failing at the call site. On 7.12.0 and above, call `generatePath` directly and delete any hand-rolled `String.replace` substitution written to work around this.

*The stale belief:* That `generatePath` only substitutes a parameter that occupies a whole path segment, so a pattern like `/books/:id.json` or `/files/:name.csv` has to be built by string replacement or a template literal.

```ts
// Stale
import { generatePath } from 'react-router'

// The workaround, because generatePath returned the pattern unchanged
// for a param followed by a literal suffix:
const url = '/books/:id.json'.replace(':id', String(book.id))

// Current
import { generatePath } from 'react-router'

// 7.12.0 and above: the suffix is preserved and the param is substituted
const url = generatePath('/books/:id.json', { id: String(book.id) })
// -> '/books/7.json'
```

> EXECUTED 2026-09-09 against the shipped packages, not read off the note: `react-router@7.11.0` returns `"/books/:id.json"` and `react-router@8.3.1` returns `"/books/7.json"` for the same call, in plain Node with no DOM. BISECTED 2026-09-09 over a 21-rung ladder, 7.9.0 to 8.3.1, one release per directory resolved alone (JOURNAL/108). The changelog's 7.12.0 is CONFIRMED as the boundary: the call returns the pattern unchanged at 7.11.0, the release immediately below it, and the substituted path at 7.12.0 and every rung above.

*Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry.*

Source: [react-router CHANGELOG.md — 7.12.0](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) · 2026-01-07 · [react-router PR #14269](https://github.com/remix-run/react-router/pull/14269)

### Wrong facts about the library

Not code — versions, minimums and metadata that models state confidently and get wrong.

#### `node_modules/react-router/docs`

**Added in react-router 7.17.0** (2026-06-04) · boundary measured on 21 releases, react-router 7.9.0 through 8.3.1

Since react-router 7.17.0 the installed package carries a subset of the official documentation as Markdown at `node_modules/react-router/docs`, so a coding agent working in a project can read that project's own version of the docs off disk instead of fetching a doc site that may describe a different release. The shipped subset is `index.md` plus `start/`, `how-to/`, `explanation/` and `upgrading/`; the auto-generated API reference (`api/`), `community/` and the tutorials are excluded.

*The stale belief:* That the `react-router` npm package ships only `dist/`, a README and a changelog, and that documentation has to come from the web.

```text
# 7.17.0 and above - the docs are in the install
ls node_modules/react-router/docs
# explanation  how-to  index.md  start  upgrading
```

> EXECUTED 2026-09-09: the directory is absent from an installed `react-router@7.11.0` and present at `8.3.1` with exactly the five entries listed. S4 rather than something higher because nothing breaks if a model does not know this - it is a capability the model would not think to use, which is the failure mode the wrong-metadata level covers. BISECTED 2026-09-09 over a 21-rung ladder, 7.9.0 to 8.3.1, one release per directory resolved alone (JOURNAL/108). The changelog's 7.17.0 is CONFIRMED: no `docs` directory at 7.16.0, the release immediately below it, and exactly the five entries at 7.17.0 and every rung above. The probe asserts the EXACT set rather than the directory's existence, so a release that also shipped the generated `api/` reference would falsify the statement instead of passing a presence check. Recorded partly because it bears on this Index's own subject: a library shipping a correction channel for coding agents inside the package.

*Reproduced against: no model yet. Verified from the primary source only — this is a correction, not an Index entry.*

Source: [react-router CHANGELOG.md — 7.17.0](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) · 2026-06-04 · [react-router CHANGELOG.md — 7.17.0](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) · 2026-06-04 · [react-router PR #15121](https://github.com/remix-run/react-router/pull/15121)

---

*Findings, code and citations: `data/react-router/` — one JSON file and one write-up per model, each finding carrying the release that broke the belief, its publication date and a verbatim quote from the primary source. Corrections: `data/react-router/facts.json`. This file is generated by `tools/build-corrections.mjs`; if the prose and the data ever disagree, that is a bug in the generator, not a stale pack.*
