react-router correction pack · for projects on react-router@^7.12 || ^8
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.
// 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 arelatest: 7.18.3(published 2026-08-28) with no 8.x version present, whilereact-router'slatestis 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 bytools/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 nomeasured_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 · 2026-06-17 · react-router CHANGELOG.md — 8.0.0 major changes · 2026-06-17 · npm registry metadata for react-router-dom
react-router@8 engine and peer requirements
New requirement in react-router 8.0.0 (2026-06-17) · boundary measured on 2 releases, react-router 7.11.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.
// 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.0declaresengines {"node":">=20.0.0"}and peersreact >=18/react-dom >=18;react-router@8.3.1declaresengines {"node":">=22.22.0"}and peersreact >=19.2.7/react-dom >=19.2.7. 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 supportsrequire()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 · 2026-06-17 · react-router CHANGELOG.md — 8.0.0 major changes · 2026-06-17 · npm registry metadata for 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 2 releases, react-router 7.11.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.
// 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]atreact-router@7.11.0and[context, params, pattern, request, url]at8.3.1— so the rename is measured in both directions, the old key absent above and the new key absent below. The list of renamed types in the second clause is from the changelog and is not executable from JavaScript. Boundary is the changelog's 7.15.0, bracketed and not bisected. BACKLOG 1g-b (c).
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 · 2026-05-05
url (loader / action / middleware argument)
Added in react-router 7.15.0 (2026-05-05) · boundary measured on 2 releases, react-router 7.11.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.
// 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
urlwas executed 2026-09-09 against the shipped packages: acreateStaticHandlerloader driven throughquery()sees argument keys[context, params, unstable_pattern, request]atreact-router@7.11.0and[context, params, pattern, request, url]at8.3.1, in plain Node with no DOM. The second half — thatrequest.urlcarries the.datasuffix on the v8 line — is taken from the 8.0.0 changelog entry removingfuture.v8_passThroughRequestsand has NOT been executed here; it needs a framework-mode server request, whichcreateStaticHandlerdoes not produce. The boundary 7.15.0 is the changelog's, bracketed by the two executed rungs and not bisected. BACKLOG 1g-b (c).
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 · 2026-05-05 · react-router CHANGELOG.md — 8.0.0 · 2026-06-17
generatePath
Behaviour changed in react-router 7.12.0 (2026-01-07) · boundary measured on 2 releases, react-router 7.11.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.
// 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.0returns"/books/:id.json"andreact-router@8.3.1returns"/books/7.json"for the same call, in plain Node with no DOM. The boundary is taken from the changelog entry for 7.12.0 (#14269) and the two executed rungs bracket it; it has NOT been bisected, so as far as measurement goes the first release where the behaviour changes is somewhere in 7.12.0-8.3.1. BACKLOG 1g-b (c) runs the ladder.
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 · 2026-01-07 · react-router PR #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 2 releases, react-router 7.11.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.
# 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.0and present at8.3.1with 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. Boundary is the changelog's 7.17.0, bracketed by the two inspected rungs and not bisected. 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 · 2026-06-04 · react-router CHANGELOG.md — 7.17.0 · 2026-06-04 · react-router PR #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.