.caveat { font-family: var(--font-mono); font-size: 12px; color: var(--ink-faint); line-height: 1.75; margin-top: 12px; padding: 16px 20px; border: 1px dashed var(--line); border-radius: 12px; } .caveat b { color: var(--ink-soft); font-weight: 650; } .caveat .k { color: var(--brand); }
AI-Readiness Scorecard
react-router · v8.3.0

React Router 8 is 93% ready for AI coding agents.

The newest breaking major on the board, and Claude Opus 5 already writes most of it. It drops json() and defer() unprompted, reaches for data() when it needs a status, and reads middleware context the v8 way. One task fails, every time: the meta function still receives the data argument that v8 removed.

93/100
Agent-readiness
14 compile clean1 fails type-check
Model: claude-opus-5Tasks: 15 Method: tsc against real installed packageRun: 2026-07-31
Prisma 7
87
client setup on v6 →
Next.js 16
92
revalidateTag arity →
React Router 8
93
meta's removed data arg
Zod 4
100
clean sweep →
The one consistent miss

meta() still gets the argument v8 deleted

v8 removed the data parameter from the meta APIs in favour of loaderData, to line up with Route.ComponentProps. The model writes the old one — and it isn't a fluke: re-run three more times, it failed 3 of 3.

Set the page title from the loader's dataTS2339 · 3 of 3 runs
Opus 5 writes (pre-v8)
export const meta: MetaFunction<typeof loader> =
  ({ data }) => {
    return [{ title: data?.user?.name ?? "User" }];
  };
React Router 8 wants ✓
export const meta: MetaFunction<typeof loader> =
  ({ loaderData }) => {
    return [{ title: loaderData?.user?.name ?? "User" }];
  };
error TS2339: Property 'data' does not exist on type 'MetaArgs<() => Promise<{ user: … }>>'.
Why it slips through review: the rename touched MetaArgs, MetaMatch, Route.ComponentProps.matches and UIMatch together, and the old name is the more natural word. A reader skims data in a meta function and sees nothing wrong. The compiler doesn't.
Clean

What it gets right — 14 of 15

The removals it could have tripped on, it didn't.

v8 deleted a lot: json(), defer(), the whole react-router-dom package, and the plain-object context. The model wrote none of them. It returns bare objects from loaders, hands back an un-awaited promise for streaming instead of defer(), wraps values in data() when it needs a 400, calls createContext() without the old unstable_ prefix, and reads middleware state with context.get(userContext) rather than as a property.

loader / actionredirect()useLoaderData useFetcheruseSubmituseSearchParams isRouteErrorResponsecreateBrowserRouter no json() — returns plain datano defer() — raw promise data() for status codescreateContext — no unstable_ context.get() — not a plain object
Six scores, one pattern

Readiness tracks the drift window

93 on a major this new is the surprise here. The pattern across this board has been that the most recent breaking change scores worst — Prisma 7 sits at 87 for exactly that reason. React Router 8 is newer still and lands at 93, with a single miss.

The plausible reason is that v8 mostly finished removals that v7 had already started deprecating: json() and defer() were on the way out long before they were deleted, so the model had years of "don't use this" signal to learn from. The one thing it gets wrong is the one thing that was a genuine rename rather than a long-signposted removal.

Which is the more useful version of the drift rule: it isn't the age of the release that predicts the gap, it's how much warning the ecosystem had.

No opinions — just the compiler

How this was measured

15 realistic React Router tasks → generated by claude-opus-5 → each written into a project with the real installed react-router v8.3.0 and type-checked with tsc --noEmit.
A task passes only if it compiles clean. Prompts name the building blocks (loader, action, middleware, useFetcher, …) but never the signatures — so this measures what the model reaches for. Failures are the compiler's own diagnostics. Reproducible: pinned versions, pinned model.
One thing this score cannot see. v8 deleted the react-router-dom package and asks you to import RouterProvider from react-router/dom. The model imported it from react-router — which still type-checks, because the root entry re-exports it. So that task passed, and the drift it was written to catch is invisible to a type-checker.

Same limit applies board-wide: tsc catches "this does not exist" and "wrong shape". It cannot catch code that compiles and means something else.

Is your SDK ready for AI agents?

Right after a major release, agents keep shipping your old API. SDKProof scores it, tracks it across model releases, and tells you exactly what to fix.

See it on GitHub →
early project · scorecards are independent analysis