81 KiB
React Router rule research
Research date: 2026-07-19
Status: implemented and validated on the accompanying branch.
Outcome
React Router is a strong candidate for a dedicated React Doctor family, but it cannot be treated as
one framework-shaped API. Its official agent skill separates Framework, Data, and Declarative
modes, and the same distinction is necessary for sound linting. In particular, loaders, actions,
fetchers, route.lazy, and route-module exports do not exist in Declarative mode.
The first pass found 15 candidates. A second pass through the router runtime invariants, middleware implementation, v6/v7/v8 changelogs, security advisories, Framework conventions, accepted design decisions, and maintainer threads expands the backlog to 38 candidates. The P0 tier is now 12 rules whose harmful behavior is directly provable and whose version boundary can be encoded; the table retains the original P1/P2 context:
| Priority | Candidate | Modes | Precision | Confidence |
|---|---|---|---|---|
| P0 | react-router-no-router-in-render |
Data | scope + path | high |
| P0 | react-router-no-navigate-in-render |
all | path | high |
| P0 | react-router-no-unsynchronized-search-params-mutation |
all | scope + path | high |
| P0 | react-router-no-multiple-set-search-params-in-tick |
all | path | high |
| P0 | react-router-no-invalid-lazy-route-properties |
Data | scope | high |
| P0 | react-router-nested-route-requires-outlet |
all | cross-file | high when resolved |
| P0 | react-router-no-loader-request-body |
Framework, Data | scope | high |
| P0 | react-router-no-session-mutation-in-loader |
Framework | scope | high |
| P0 | react-router-no-use-loader-data-in-error-ui |
Framework, Data | scope + project | high |
| P0 | react-router-no-multiple-middleware-next |
Framework, Data | path | high |
| P0 | react-router-descendant-routes-require-splat |
all | cross-file | high when resolved |
| P0 | react-router-no-invalid-absolute-child-path |
all | project | high when static |
| P1 | react-router-require-root-error-boundary |
Framework, Data | project | high |
| P1 | react-router-guard-aborted-handle-error |
Framework | path | high |
| P1 | react-router-resource-link-requires-reload |
Framework, Data | project + scope | high when resolved |
| P1 | react-router-loader-parallel-fetch |
Framework, Data | path | high |
| P1 | react-router-loader-fetch-forwards-signal |
Data, client loaders | scope | medium-high |
| P1 | react-router-prefer-route-lazy |
Data | scope | medium-high |
| P2 | react-router-internal-route-anchor |
all | project + scope | medium-high when resolved |
| P2 | react-router-csp-nonce-consistency |
Framework | cross-file | medium-high |
| P2 | react-router-valid-route-object |
Data | scope | high, but mostly type-covered |
The number is intentionally not the goal. The expansion is useful because it separates three different products that should not be conflated:
- default-on behavioral rules with direct runtime or security consequences;
- version-scoped migration rules that only activate for a declared target major;
- dependency/configuration diagnostics better implemented in the existing supply-chain or project scanners than as AST lint rules.
“When resolved” is load-bearing. The project-aware rules should remain silent when React Doctor cannot prove the route target or component implementation. A missing diagnostic is preferable to guessing that an anchor points to a UI route, that an imported component omits an outlet, or that a URL is a resource route.
Implementation status
All 38 source diagnostics in this document are registered. Project discovery resolves
@react-router/dev, react-router-dom, and react-router, emits monotonic capabilities at each
release boundary used by a rule, distinguishes Framework mode from non-Framework package use, and
chooses the lowest parseable version in a mixed-version workspace. An unparseable workspace or
catalog spec receives only the bare react-router capability, so version-specific rules fail
closed. Data- and Declarative-mode rules require local syntax proof from imported APIs instead of a
project-wide mode guess. Every rule also checks the nearest package boundary before creating
visitors.
The implementation deliberately constrains cross-file contracts to same-file static evidence: inline route objects, inline component bodies, literal destinations, proven imports, and recognized Framework entry/config filenames. It does not guess through opaque route factories, imported components, dynamic options, or arbitrary helper calls. Dependency advisories remain in the existing supply-chain scanner instead of being duplicated as AST rules.
Validation covered the plugin's full regression suite, strict invariant fuzzing, a 100-repository
cross-framework corpus, a React Router 6.30 SPA, a 7.0 Data-mode app, a 7.16 Framework app, the
official stable-middleware playground, and the official v8 template. The field pass narrowed two
detectors before release: loader parallelization now requires independent native fetch calls,
and environment-suffixed route modules require a default route component rather than any colocated
named export.
Required foundation
Mode-aware capability detection
The shipped foundation uses project capabilities only where package evidence is reliable:
react-router:react-routerorreact-router-domis installed.react-router-framework:@react-router/devis installed.- monotonic version tokens:
react-router:6.4throughreact-router:8at the release boundaries used by the rules.
The official skill provides the exact mode signals and warns not to apply Framework/Data guidance
to Declarative apps. Data and Declarative usage are therefore proven locally from route creators,
route objects, JSX routers, and imported APIs. Filename-sensitive Framework rules stay limited to
canonical files; resolving custom appDirectory and arbitrary route configs remains future
cross-file-index work.
Source compatibility matters:
- React Router 6 applications commonly import DOM APIs from
react-router-dom. - React Router 7 collapsed the APIs into
react-routerandreact-router/domwhile retaining the compatibility package. - React Router 8 removed
react-router-dom.
Rules should resolve imports from both packages, then use the installed major only where behavior actually differs. The current documentation is v8.2.0, while the contracts below intentionally target stable Data APIs from v6.4 onward and Framework APIs from v7 onward.
Version resolver and rule gates
Version-aware does not mean checking the import spelling. The shipped resolver reads declared versions and catalog references from root and workspace manifests, chooses the lowest parseable version conservatively, and treats an unresolved or non-semver dependency as unknown. A rule may then choose one of four policies:
- API-presence gate: activate from the first version that shipped the API.
- Behavior gate: activate only from the release that made the diagnosed behavior true.
- Target-major migration gate: activate only when the user explicitly targets the next major, or when the installed major has already removed the construct.
- Advisory gate: compare the installed package to a patched range in the supply-chain scanner; do not pretend a vulnerable dependency is a source-code lint problem.
| Feature or behavior | v6 | v7 | v8 | Rule consequence |
|---|---|---|---|---|
| Data routers, loaders, actions | >=6.4 |
supported | supported | Data-only rules require Data/Framework proof, not merely a package import. |
route.lazy function |
>=6.9 |
supported; object form added later | supported | Lazy-property and route-lazy rules stay off below 6.9. |
useBlocker |
unstable >=6.7, stable >=6.19 |
supported | supported | Multiple-blocker rule recognizes both names only in their valid ranges. |
| Search-param setter callback isolation | shared mutable instance before 7.7 | copied callback value from 7.7.0 |
copied value | The mutation rule follows only the tuple result, never the setter callback parameter, so the same detector is valid across versions. |
| Middleware | absent | unstable before 7.9; stable >=7.9 |
always enabled | Stable middleware rules gate at 7.9; experimental middleware remains outside the first shipped contract. |
next() never throws |
absent | true from 7.8.0 |
true | The try/catch-around-next rule must stay off for older experimental middleware. |
| React transition router option | absent | unstable_useTransitions from 7.10; useTransitions from 7.15 |
supported as useTransitions |
Promise-return rule uses the correct prop name and only treats Data/Framework navigation APIs as promise-returning. |
Framework root Layout error flow |
absent | supported | supported | Loader-data-in-error-UI applies to Framework root Layout and route boundaries from v7. |
| Package entry points | react-router-dom normal |
compatibility export retained | react-router-dom removed |
The import migration rule activates only for an installed v8; maintained v6/v7 apps remain quiet. |
meta/useMatches match data |
data |
data deprecated from 7.8; loaderData available |
data removed |
The removed-field rule activates only for an installed v8 and requires scope or Framework-export proof. |
future.v8_* flags |
absent | opt-in upgrade controls | removed or promoted | The removed-flag rule activates only for an installed v8 Framework config. |
Two important non-rules follow from this matrix:
- Do not run a v8 codemod-style diagnostic merely because current documentation prefers the v8
spelling. A maintained v6 application is correct to import from
react-router-dom. - Do not fork every behavioral detector by major. Most route-tree invariants are unchanged from v6 through v8; attach a minimum version only when the API or behavior genuinely differs.
Route and component index
Three candidates need a small cross-file index:
- route parent -> component/module;
- route path -> UI route or resource route;
- component -> directly or transitively renders
Outlet/callsuseOutlet.
Framework mode can derive this from routes.ts, @react-router/fs-routes, and the configured app
directory. Data mode can derive it from static route objects supplied to a data-router creator.
Declarative mode can derive it from Route elements. Dynamic route factories, runtime patching,
and opaque third-party configs stay unknown.
Evidence base
Primary React Router sources
- The official React Router agent skill
defines the mode boundary and says to use installed, version-matched docs. It was added to
create-react-routerin v8.1.0. - Picking a Mode explains that Data mode moves route config outside React rendering and adds loaders/actions/fetchers on top of Declarative mode.
- Data-mode installation says to create a data router once outside the React tree and not hold it in React state.
- RouterProvider explicitly warns against creating new routers during renders and re-renders.
- useSearchParams documents both the stable, mutable object and the lack of React-style queueing for repeated setter calls in one tick.
- The
route.lazydesign decision explains why path-matching fields cannot be returned bylazyand why static handlers can run in parallel with lazy module loading. - Data routing states that child routes render through
the parent
Outletand that index routes cannot have children. - Error boundaries says every application should export at least a root error boundary and documents boundary bubbling.
entry.server.tsxsays aborted requests generally should not be logged or reported fromhandleError.- Resource routes require an anchor or
Link reloadDocument; an ordinaryLinkmakes the router try to render the payload as UI. - Custom Data frameworks demonstrate forwarding
request.signalto a loader fetch. - Security requires the same CSP nonce on
ServerRouterand the React streaming renderer. - Middleware says
next()may be called only once, never throws from v7.8 onward, and requires the server middleware to return the response. - Sessions and Cookies warns that
session mutation in a loader creates CSRF exposure, explains the required outgoing
commitSession/destroySessioncookie, and recommendsmaxAgeover a module-static expiry. - Root route convention forbids
useLoaderDatainErrorBoundaryand extends the restriction to rootLayout, which also renders error flows. - Client modules says exports
from
.clientmodules areundefinedon the server and should only be consumed in effects and user-event callbacks. - Server modules says route
modules cannot use
.serveror.clientsuffixes because the compiler needs them in both graphs. - React Transitions requires returning or
awaiting an imperative navigation promise inside
startTransition. - The accepted Do not clone request decision defines loaders as GET/HEAD handlers with no request body.
- Runtime route conversion and matching enforce unique route IDs, valid absolute-child prefixes,
index routes without children, splats after
/, one router, and one blocker. - Updating from v7 and the
changelog provide the v8 import,
loaderData, future flag, runtime baseline, and security-patch boundaries used by the version matrix.
Maintainer issues, pull requests, and discussions
- Discussion #10605 diagnoses duplicate
loaders under Strict Mode when
createBrowserRouterruns in a component render and directs the user to module scope. - Issue #11622 led to the official warning
that the stable
searchParamsreference is mutable and can diverge from the URL. - Issues #14491 and
#14077 confirm that repeated
setSearchParamscalls in one tick intentionally do not compose like React state updates. - PR #10435 fixed Strict Mode behavior for
Navigatein a data router. It is evidence not to banNavigate; descendantRoutesin a Data app still do not participate in loaders and may need the component. - Discussion #10465 recommends
route.lazyoverReact.lazyfor a data route because the latter introduces a render-fetch waterfall. - Remix issue #5846 contains a concrete nested
route that never renders because its parent omits
Outlet. - PR #12601 added the nuanced
clientLoader.hydrate/HydrateFallbackdocumentation; this evidence rules out an unconditional “hydrate requires fallback” rule. - Discussion #9841 and the router's
own warning demonstrate that a component-owned descendant
Routestree requires the parent path to end in/*. - PR #14118 changed middleware error
handling in v7.8 so
next()returns a boundary response instead of throwing. - PR #14215 stabilized middleware and context APIs in v7.9.
- PR #14524 introduced transition-enabled
routers in v7.10, and PR #14999 stabilized
the
useTransitionsspelling in v7.15. - The accepted useBlocker decision explicitly limits a router to one active blocker.
Skills and ecosystem review
- The official skill is mode- and version-aware. Its strongest lintable guidance is represented in this backlog: router creation outside render, loader/action data flow, outlet nesting, route lazy loading, URL parsing, and root error handling.
- The community frontend React Router best-practices skill supplied useful seeds around parallel loaders, abort signals, resource routes, forms/fetchers, redirects, and hydration. Its organizational and preference-level advice was not treated as a rule contract without primary corroboration.
- The old
eslint-plugin-react-routerpackage predates modern data routers. Putout offers React Router transforms, and TanStack Router has a router-specific ESLint plugin, but there is no mature modern React Router lint suite covering these contracts. React Doctor would be filling a real gap rather than cloning an established ruleset.
P0 contracts
react-router-no-router-in-render
Rule definition:
This rule catches a data-router instance created inside a component or hook render lifecycle and
passed to RouterProvider, which recreates router state and can rerun loaders.
Runtime reason: A data router owns navigation, loader, fetcher, and subscription state. Recreating it during React render discards that state; Strict Mode can create it twice and execute initial loaders twice.
Detector precision: Scope-aware plus render-path analysis.
Evidence:
- Data installation and
RouterProviderrequire one instance outside the React tree. - Discussion #10605 reproduces duplicate loaders and the maintainer identifies render-time router construction as the cause.
Strong positives:
const router = createBrowserRouter(routes)directly inApp, returned through<RouterProvider router={router} />.- A router held in
useStateoruseMemoand passed toRouterProvider; the official docs reject React-owned router lifetime, not just the direct-call spelling. - Aliased imports of
createBrowserRouter,createHashRouter, orcreateMemoryRouter.
False-positive traps:
- Module-scope router construction is correct.
createStaticRouterper server request is correct and is not part of this rule.- A factory mentioned inside a component but only executed by an unrelated deferred callback and
never supplied to
RouterProvidershould stay quiet. - Test helpers may intentionally create isolated memory routers; either exclude test-like files or tag the rule for test noise.
In scope:
- Proven React Router creator imports whose result reaches the provider in the same file.
- Direct calls and React initializer callbacks.
Out of scope:
- Opaque custom router factories, cross-file value flow, and server static routers in v1.
Test seeds:
- Invalid: direct render call, aliased creator,
useMemo,useStateinitializer, helper invoked on the render path. - Valid: module scope, server request
createStaticRouter, unused deferred callback, shadowed localcreateBrowserRouter.
Open questions:
- Whether test files should be excluded or reported under
test-noise.
react-router-no-navigate-in-render
Rule definition:
This rule catches the function returned by useNavigate being invoked on the synchronous render
path, which causes navigation as a render side effect.
Runtime reason: Render can restart or execute more than once. A navigation there can duplicate loader work, produce hydration inconsistencies, or trigger update-during-render failures.
Detector precision: Scope-aware path analysis.
Evidence:
useNavigateis documented for user interactions or effects.- Discussion #10605 recommends links or loader redirects instead of navigation from render.
Strong positives:
if (!user) navigate("/login")in a component body.- A local helper called synchronously from render that invokes the proven navigate binding.
- Calls inside an IIFE, array iterator, or
startTransitionexecuted during render.
False-positive traps:
- Event handlers, effects, promise continuations, timers, and callbacks returned from custom hooks.
<Navigate>is a separate supported API and must not be flagged.- A locally declared or shadowed function named
navigate.
In scope:
- Named, aliased, and namespace imports of
useNavigatefrom supported React Router packages. - Direct and transitively invoked local synchronous functions.
Out of scope:
- Opaque imported helpers and deciding whether an effect should be converted to a loader redirect.
Test seeds:
- Invalid: direct conditional call, IIFE, called helper,
forEach,startTransition. - Valid: click handler, effect, timeout,
.then, returned hook callback, shadowed hook/function.
Open questions:
- None. Reuse the hardened deferred-callback analysis from
tanstack-start-no-navigate-in-render.
react-router-no-unsynchronized-search-params-mutation
Rule definition:
This rule catches mutation of the URLSearchParams object returned by useSearchParams when the
same synchronous update does not call its paired setter, leaving the object and URL out of sync.
Runtime reason: The hook returns a stable but mutable reference. Mutating it can change values observed on a later render even though the browser URL never changed.
Detector precision: Scope-aware path analysis.
Evidence:
- The current hook documentation states the exact object/URL divergence.
- Issue #11622 records the maintainer clarification that became the documentation.
Strong positives:
searchParams.set,append,delete, orsorton the first tuple binding with no pairedsetSearchParamscall in the same synchronous update.- Mutation through a local alias of the proven hook result.
False-positive traps:
- Mutating the callback parameter inside
setSearchParams(previous => { ...; return previous })is an official supported example. const next = new URLSearchParams(searchParams); next.set(...)is valid.- Mutating the outer object and then passing it to the paired setter in the same path updates the URL and should stay quiet, even if cloning is stylistically cleaner.
- An unrelated
URLSearchParamsinstance or shadoweduseSearchParams.
In scope:
- Direct mutating methods on the proven first tuple element and simple local aliases.
- Pairing with the second tuple element from the same hook call.
Out of scope:
- Mutation hidden in imported helpers or passed through arbitrary object graphs.
Test seeds:
- Invalid: each mutator without setter, alias mutation, mutation in a later event callback without setter.
- Valid: setter callback mutation, clone mutation, mutation followed by paired setter, read-only methods, shadowed hook.
Open questions:
- Whether mutation followed by the setter only on some control-flow branches should report at the mutation or at the unpaired exit.
react-router-no-multiple-set-search-params-in-tick
Rule definition:
This rule catches two reachable calls to the same setSearchParams binding in one synchronous
execution segment, where later navigation overwrites rather than composes with the earlier update.
Runtime reason: Unlike React state, functional search-param updates are not queued. Multiple calls in the same tick start from the same location and do not build on one another.
Detector precision: Path-aware.
Evidence:
- The behavior is an explicit warning in the hook documentation.
- Issues #14491 and #14077 were closed as expected behavior and point users to one combined update or intermediate React state.
Strong positives:
- Two setter calls in sequence in one event/effect/function.
- A setter call followed by a synchronously invoked local helper that calls the same binding.
False-positive traps:
- Calls in mutually exclusive branches.
- A later call after
await, in a timer, or in a promise continuation. - Calls to setters from different
useSearchParamsinstances. - A deliberately redundant first call is dead but not necessarily a router correctness issue; v1 may still report because its result is provably discarded, with a precise message rather than a claim about user intent.
In scope:
- Same-file direct calls and synchronously invoked local helpers.
- One diagnostic on the later call.
Out of scope:
- Cross-module helper effects and scheduling through unknown APIs.
Test seeds:
- Invalid: sequential object updates, sequential functional updates, called local helper.
- Valid: combined callback, exclusive branches, post-
await, timeout, different setters.
Open questions:
- Whether to begin with direct same-function calls only, then add local helper expansion after OSS evaluation.
react-router-no-invalid-lazy-route-properties
Rule definition:
This rule catches a Data route's lazy result attempting to define path, index,
caseSensitive, children, or id, which React Router cannot apply after matching.
Runtime reason:
React Router must match and uniquely identify a route before it knows which lazy function to
load. Returning matching fields is too late and is ignored with a warning.
Detector precision: Scope-aware.
Evidence:
- The route-lazy design decision names the immutable fields and explains the ordering constraint.
createBrowserRouterdocumentation keeps route definitions such as path/index available up front while lazily loading implementation fields.
Strong positives:
- A route object passed to a data-router creator whose inline
lazyfunction returns any forbidden field. - A
lazy: () => import(...)module that can be resolved locally and exports one of the forbidden route fields.
False-positive traps:
- An arbitrary object with a property named
lazy. - Valid lazy fields such as
Component,ErrorBoundary,loader,action,handle, andshouldRevalidate. - Static route fields beside
lazyare correct.
In scope:
- Inline object returns first; locally resolvable imported route modules as a follow-up.
Out of scope:
- Dynamic imports whose exports cannot be statically resolved.
Test seeds:
- Invalid: each forbidden field, async return, aliased data-router creator.
- Valid: forbidden fields on the static route object, valid implementation fields from lazy,
unrelated
lazyobject, shadowed creator.
Open questions:
- None for inline returns.
react-router-nested-route-requires-outlet
Rule definition:
This rule catches a route that both renders a parent component and owns child routes when the
resolved parent component never renders Outlet and never calls useOutlet.
Runtime reason: Matched child UI is inserted only at the parent's outlet. Without one, the URL can match while the child is never visible.
Detector precision: Project-aware cross-file resolution.
Evidence:
- Data routing documents the outlet as the child render point.
- Remix issue #5846 demonstrates a matched nested route hidden by a parent without an outlet.
Strong positives:
- A Data route object with
Componentpluschildren, where the local component tree contains no outlet. - A declarative parent
Route element={<Layout />}>with nestedRoutechildren and a resolvable outlet-free layout. - A Framework route module used as a parent in
routes.tswith no transitive outlet.
False-positive traps:
- A prefix route with children but no component needs no outlet.
- A component that delegates to a local/imported child which renders
Outletis valid. useOutlet()rendered as a value is valid.- An opaque external component must remain unknown and quiet.
In scope:
- Statically indexed routes and components, with transitive local component traversal.
Out of scope:
- Runtime-patched routes, arbitrary route factories, and unresolved external layouts.
Test seeds:
- Invalid: local parent without outlet in all three modes, fragment-only layout, unrelated imported symbol named Outlet.
- Valid: direct Outlet, aliased Outlet,
useOutlet, delegated local child, prefix route, leaf route, unresolved external component.
Open questions:
- Set a conservative traversal depth and cycle handling policy for delegated component trees.
P1 contracts
react-router-require-root-error-boundary
Rule definition: This rule catches a Framework or Data application whose statically known root route coverage has no React Router error boundary.
Runtime reason: Without an application boundary, unexpected loader, action, lazy-module, and render errors fall to the minimal built-in UI instead of an application-owned recovery surface.
Detector precision: Project-aware.
Evidence:
- The error-boundary guide says every application should at minimum export a root boundary and shows both Framework and Data forms.
Strong positives:
- Framework
root.*without namedErrorBoundaryexport. - A Data router with a single root route lacking
ErrorBoundary/errorElement. - Multiple top-level Data branches where at least one branch has no ancestor boundary covering it.
False-positive traps:
- Nested routes do not each need a boundary; errors bubble to the nearest ancestor.
- Declarative
Routesdo not participate in data-router error handling. - A custom Framework app directory must be honored.
In scope:
- Framework root modules and static Data route trees.
Out of scope:
- Runtime-patched route trees and generic React class error boundaries not registered with the router.
Test seeds:
- Invalid: Framework root without export, Data root without either boundary form, uncovered second root branch.
- Valid: named Framework export, Data
ErrorBoundary, DataerrorElement, nested routes covered by an ancestor, Declarative app.
Open questions:
- Whether the first release should support only Framework mode for a simpler, zero-ambiguity project check.
react-router-guard-aborted-handle-error
Rule definition:
This rule catches known logging or error-reporting sinks in Framework handleError that are not
dominated by a request.signal.aborted guard.
Runtime reason: React Router aborts interrupted requests as normal concurrency behavior. Reporting those requests creates false incidents and noisy logs.
Detector precision: Path-aware.
Evidence:
- The
entry.servercontract explicitly recommends skipping aborted requests and provides the guard.
Strong positives:
- Unconditional
console.error, SentrycaptureException, or a known logger/reporting import in exportedhandleError. - Reporting in a branch that can still execute when
request.signal.abortedis true.
False-positive traps:
if (request.signal.aborted) returnbefore the sink.if (!request.signal.aborted) { report(error) }.- Pure formatting, metrics explicitly describing abort volume, and an unrelated function named
handleErroroutside the server entry/module contract.
In scope:
- Framework server entry files and typed/named
handleErrorexports; globalconsole.errorand Sentry reporting imports. Nested function bodies are excluded unless execution is proven.
Out of scope:
- Unknown side-effect functions whose reporting semantics cannot be established.
Test seeds:
- Invalid: unguarded console, Sentry, logger import, partial guard.
- Valid: positive guard, early return, no sink, client
onError, shadowed request/signal.
Open questions:
- Curate the initial reporting-sink imports from existing React Doctor telemetry knowledge rather than guessing from function names alone.
react-router-resource-link-requires-reload
Rule definition:
This rule catches Link or NavLink client navigation to a statically known resource route without
reloadDocument.
Runtime reason: A resource route returns a file or raw response, not route UI. Client routing tries to fetch and render that payload through the data-router protocol and fails.
Detector precision: Project-aware plus scope resolution.
Evidence:
- The resource-route guide explicitly requires
<a>or<Link reloadDocument>. - PR #8283 introduced the intentional full-document escape hatch.
Strong positives:
- A literal or statically generated
tothat resolves to a route module with loader/action but no default component. - A Data route object target with a loader/action and no render property.
False-positive traps:
- Native anchors are correct for resource routes.
reloadDocument, external URLs, unresolved dynamic targets, and UI routes.- A path-only parent route with children is not a resource route.
In scope:
- Exact static targets proven by a loader/action-only Data route object in the same module. This conservative first release requires React Router 6.4 or newer.
Out of scope:
- Cross-file Framework route discovery, parameterized targets, runtime-generated routes, and arbitrary string construction. These require a project route index before they can be enabled without reviving the file-extension false positive.
Test seeds:
- Invalid: Link/NavLink to static and parameterized resource paths.
- Valid: native anchor, reloadDocument, UI route, external URL, unresolved target, shadowed Link.
Open questions:
- Whether Data-mode resource classification should require the loader to return a proven
Response, reducing coverage but avoiding component-less prefix routes.
react-router-loader-parallel-fetch
Rule definition: This rule catches independent sequential awaits in a route loader or client loader that create an avoidable request waterfall.
Runtime reason: Route rendering waits for loader completion. Independent work serialized by separate awaits adds latencies instead of taking their maximum.
Detector precision: Path-aware dependency analysis.
Evidence:
- Route object documentation demonstrates
Promise.allinside lazy loading, and React Router runs matched loaders in parallel. - The official and community skills recommend parallel independent loader work.
Strong positives:
- Two or more pairwise-independent top-level awaits in a Framework
loader/clientLoader. - Independent awaits in a loader property of a proven Data route.
False-positive traps:
- The second operation consumes data, or a transitively derived binding, from the first.
- Sequential mutation/transaction semantics.
- Nested callbacks that are already scheduled concurrently.
In scope:
- Reuse and generalize the dependency-aware implementation in
tanstack-start-loader-parallel-fetchrather than fork it.
Out of scope:
- Imported helper internals and performance claims about a single await.
Test seeds:
- Invalid: independent fetches, aliased route exports, three awaits with an independent sibling pair.
- Valid: direct/transitive dependency, control-flow alternatives,
Promise.all, non-loader.
Open questions:
- Prefer a shared
loader-parallel-fetchdetector with framework-specific entry recognizers over a copy of the TanStack rule.
react-router-loader-fetch-forwards-signal
Rule definition:
This rule catches a direct fetch in a Data loader or browser-executed client loader that receives
the route request but does not forward its abort signal.
Runtime reason:
React Router cancels interrupted navigations. Without request.signal, the underlying request
continues consuming network/server resources after its result is no longer usable.
Detector precision: Scope-aware.
Evidence:
- The custom Data framework guide's loader fetch explicitly uses
{ signal: request.signal }. - React Router's race-condition model and server request APIs propagate abortable Fetch Requests.
Strong positives:
fetch(url)orfetch(url, { ...options })in a proven loader that destructures/receivesrequestbut omits its signal.- Aliased request parameters and namespace/global fetch.
False-positive traps:
fetch(request)already carries the request signal.fetch(new Request(url, { signal: request.signal }))and composed option objects proven to carry it.- Server-only database calls are not fetch.
- Fire-and-forget mutations have different cancellation semantics; v1 should target loaders and client loaders, not actions.
In scope:
- Direct native fetch and statically inspectable options.
Out of scope:
- Axios/custom clients, opaque wrappers, actions, and cases where no route request parameter is available.
Test seeds:
- Invalid: missing second arg, options without signal, unrelated signal.
- Valid: request signal,
fetch(request), Request wrapper, database call, action, custom client.
Open questions:
- Whether Framework server loaders should be included immediately or only where the downstream fetch is known to honor AbortSignal.
react-router-prefer-route-lazy
Rule definition:
This rule catches a Data route component supplied through a binding created by React.lazy when
the route could load its implementation with route.lazy before render.
Runtime reason:
React.lazy begins loading at render time, after route matching/data work, creating a render-fetch
waterfall. route.lazy lets the router load route implementation before rendering and coordinate it
with data work.
Detector precision: Scope-aware.
Evidence:
- Maintainer guidance in discussion #10465 explicitly recommends
route.lazyfor this reason. - The route-lazy design and official examples load route implementation fields together.
Strong positives:
- A
React.lazybinding used asComponent/elementin a static route object passed to a data router. - The equivalent JSX route produced by
createRoutesFromElementsand passed to a data router.
False-positive traps:
- Declarative
Routescannot use data-router route lazy;React.lazyis valid there. - A lazy component nested inside a route component is not itself the route implementation.
- Framework mode already performs route-module code splitting and should not receive Data-mode rewrite advice.
In scope:
- Proven React
lazybindings used directly as Data route render fields.
Out of scope:
- Custom lazy wrappers and deciding whether a small route is worth splitting.
Test seeds:
- Invalid: Component and element forms, aliases, createRoutesFromElements feeding a data router.
- Valid: Declarative Route, nested lazy child, Framework route module, ordinary component, shadowed lazy.
Open questions:
- Confirm the diagnostic can offer a safe non-autofix migration shape for both default and named module exports.
P2 and conditional candidates
react-router-internal-route-anchor
Only report a literal/static native anchor when the route index proves it targets a UI route.
Exclude resource routes, external/protocol-relative URLs, downloads, _blank, same-document hashes,
and explicit full-reload conventions. The existing TanStack anchor rule has useful syntax handling,
but a React Router version without route-target proof would false-positive on the exact native
anchor required for resource routes.
react-router-csp-nonce-consistency
When a Framework server entry creates or receives a CSP nonce, require the same value to reach both
ServerRouter nonce and the nonce option of renderToPipeableStream or
renderToReadableStream. Do not require nonce props merely because Scripts or
ScrollRestoration exists; a project without nonce-based CSP is valid, and current Framework mode
inherits the ServerRouter nonce for those components. Pair each render call with the single
ServerRouter in its render argument, and treat immutable identifier aliases as the same nonce.
react-router-valid-route-object
For proven Data route objects, catch API contradictions that JavaScript users otherwise encounter
at runtime: an index route with children, element with Component, errorElement with
ErrorBoundary, and hydrateFallbackElement with HydrateFallback. A path is valid on the
router's current IndexRouteObject and is not a contradiction. Keep this below the behavioral rules
because TypeScript already rejects most cases. lazy immutable fields remain a separate rule
because they are structurally valid objects whose values are ignored too late. Match the router's
truthiness semantics: explicit undefined, null, false, 0, empty-string, and void values do
not create a contradiction.
Additional high-confidence contracts
react-router-no-loader-request-body
Rule definition:
Report body-consuming methods on the loader's proven request binding. React Router loaders are
GET/HEAD handlers; request parsing belongs in an action.
Runtime reason:
The accepted request decision removes the body before loaders run. Calls such as
request.formData() or request.json() cannot recover data that is not present and usually signal
that a mutation endpoint was placed in the wrong route API.
Detector precision: Scope-aware. Start at v6.4 for Data mode and v7 for Framework route modules.
Strong positives:
request.formData(),json(),text(),blob(), orarrayBuffer()on the destructured loader request.- The same request binding passed to a local helper whose body immediately consumes it, when the existing cross-file probe can resolve that helper.
False-positive traps:
- Body reads in
action,clientAction, a resource handler for a mutation method, custom server middleware, or an unrelated function namedloader. request.url, headers, method, signal, or cloning for a non-body reason.- A framework abstraction whose own
requestis not the React Router loader argument.
Implementation boundary: No autofix. Moving code from a loader to an action changes the HTTP and UI contract.
react-router-no-session-mutation-in-loader
Rule definition:
Report proven session writes in a Framework loader: session.set, session.unset,
session.flash, or a loader return that destroys the incoming session.
Security reason: Loaders answer GET requests and can be triggered cross-site. React Router's session guide explicitly warns that logout or any session mutation in a loader creates CSRF exposure.
Detector precision:
Scope-aware provenance from getSession or a known React Router session-storage factory. Framework
mode only by default.
Strong positives:
- A session obtained from
getSession(request.headers.get("Cookie"))and mutated insideloader. destroySession(session)returned from a loader response.- A loader helper that mutates the same resolved session object.
False-positive traps:
session.get,session.has, auth checks, or redirecting an already-authenticated user.- All mutation in actions.
- Reading a flash value. This can race across nested loaders, but reading is not the CSRF issue and should be a separate conditional diagnostic if pursued.
- A method named
seton an unrelated object.
Implementation boundary: No autofix. The safe repair generally adds a POST form/action and may affect progressive enhancement.
react-router-no-use-loader-data-in-error-ui
Rule definition:
Report useLoaderData() inside a proven route ErrorBoundary and inside the Framework root
Layout. Recommend useRouteLoaderData(routeId) or generated boundary props and require the
possibly-undefined result to be guarded.
Runtime reason:
The failing loader may be the operation that selected the boundary, so happy-path loader data may
not exist. Root Layout also renders ErrorBoundary children; if it throws while reading absent
data, React Router falls back to its minimal built-in boundary.
Detector precision: Scope plus route-module/export proof. Framework v7+ and statically associated Data boundaries.
Strong positives:
- A named
ErrorBoundaryexport calling an imported or aliaseduseLoaderData. - A root
Layoutexport calling it directly or through a local custom hook that resolves to it. - An
errorElement={<Boundary />}whose resolved component calls the hook.
False-positive traps:
- The normal route component,
HydrateFallback, or a component merely namedErrorBoundarywith no route association. useRouteLoaderData("root")followed by a guard.- Loader data passed as an explicitly optional prop from a parent boundary.
Implementation boundary: Do not offer a blind hook rename: the replacement needs a route ID and undefined handling.
react-router-no-multiple-middleware-next
Rule definition:
Report a middleware function with two calls to its own next continuation that can execute on the
same path.
Runtime reason:
React Router throws You may only call next() once per middleware on the second call. next is a
single continuation through the remaining middleware and handler chain, not a reusable function.
Detector precision: Scope-aware path analysis. Stable middleware from v7.9 and all v8; optionally recognize the unstable v7 spelling only when its future flag is proven.
Strong positives:
- Two sequential
await next()calls. - One call before a branch and another on a reachable branch.
- Calling an alias of
nextand thennextitself.
False-positive traps:
- Mutually exclusive branches where each complete path invokes
nextat most once. - Nested callbacks that are never invoked by the middleware path.
- Two different middleware functions, each with its own continuation.
- An unrelated local function named
next.
Implementation boundary:
No autofix. The second call may need deletion, or the middleware may need to reuse the first
Response.
react-router-no-catch-middleware-next
Rule definition:
From v7.8 onward, report a try/catch whose protected operation is only await next() and whose
catch is intended to handle downstream route or middleware errors.
Runtime reason:
React Router catches downstream errors, renders the appropriate route boundary, and returns its
Response through next(). The catch is unreachable for those errors and creates a false sense of
recovery or logging coverage.
Detector precision:
Scope-aware syntax plus a hard behavior gate at v7.8. The narrow first version reports only when the
try body contains await next() with no other operation that can obviously throw.
Strong positives:
try { return await next() } catch (error) { report(error) }in stable middleware.- A catch that converts the supposed downstream error to a redirect or fallback response.
False-positive traps:
- Code in the try body before or after
next()that can itself throw. - Middleware versions before 7.8, where the experimental behavior differed.
- A catch around processing of the returned
Response, such as parsing a header with an API that can throw.
Implementation boundary:
Recommend an ErrorBoundary, handleError, or instrumentation according to intent; no autofix.
react-router-descendant-routes-require-splat
Rule definition:
When a route component itself renders a new descendant <Routes> tree or calls useRoutes, require
the route that mounts that component to end its static path with /*.
Runtime reason: Without the trailing splat, the parent stops matching at its own path and deeper descendant URLs never reach the nested route tree. React Router emits this exact runtime warning.
Detector precision: Cross-file route/component resolution. Applies from v6 onward in every mode.
Strong positives:
<Route path="account" element={<AccountRoutes />} />whereAccountRoutesrenders<Routes>.- A Data route
path: "account"whose resolvedComponentcallsuseRoutes. - Transitive local wrappers when the route index can prove the descendant tree is rendered.
False-positive traps:
- Ordinary nested
<Route>children rendered throughOutlet; those do not require this rule. - A component that renders
Outletbut no newRoutes/useRoutestree. - Dynamic paths or opaque third-party components that cannot be resolved.
- A leaf route that merely imports
Routesfor tests or types.
Implementation boundary:
A safe suggestion can show path="account/*", but avoid autofix because route-relative links and
tests may need coordinated updates.
react-router-no-invalid-absolute-child-path
Rule definition: Report a static absolute child path that does not begin with the fully combined path of its static parents.
Runtime reason: React Router throws during route flattening because an absolute child cannot escape the path where it is nested.
Detector precision: Project-aware route-tree evaluation. Applies from v6 onward.
Strong positives:
- Parent
/appwith nested child/settings. - A nested route-object factory whose resolved absolute path ignores a static ancestor segment.
False-positive traps:
- Valid parent
/appwith child/app/settings. - Relative child
settings. - Optional parent segments, pathless layouts, dynamically patched routes, and helper-produced paths until the index models the router's own explode/flatten semantics exactly.
- Descendant
<Routes>absolute-path limitations, which are related but are a separate contract.
Implementation boundary: The first version should skip any ancestor containing optional or dynamic syntax it cannot normalize with parity to the router implementation.
react-router-session-mutation-requires-commit
Rule definition:
On each action exit path after a proven session mutation, require the resulting response to carry a
Set-Cookie value derived from commitSession(session); after session destruction, require
destroySession(session) instead.
Runtime reason: Session objects are in-memory request values. The mutation is not persisted until the serialized cookie is included in the outgoing response.
Detector precision: Scope-aware, path-sensitive provenance. Framework mode. Report only direct response construction that the analyzer can follow.
Strong positives:
session.set(...)followed byreturn redirect("/")with no headers.destroySession(session)is called but its result is ignored.- One conditional return commits the session while another mutated path forgets it.
False-positive traps:
- A response helper whose resolved implementation adds the correct header.
- Database-backed session mutation still requires the cookie on create/delete, but existing-session updates may have custom semantics; remain quiet when the storage implementation is opaque.
- A session read with no mutation.
- Returning a
Responsesupplied by middleware that centrally commits the session.
Implementation boundary:
No autofix. The first release follows direct commitSession(session) results and immutable local
bindings into a Set-Cookie property inside the returned expression. Response helpers, mutable
header objects, and cross-function propagation stay unknown. Header merging is application-specific
and an incorrect fix can drop existing headers.
react-router-return-navigation-promise-in-transition
Rule definition:
In a transition-enabled router, report a Data/Framework imperative navigation or submission promise
called inside startTransition without being returned or awaited.
Runtime reason: React ends the transition when the callback completes. Dropping the navigation promise ends pending and optimistic transition state before the navigation finishes.
Detector precision:
Scope-aware callback control flow plus project configuration. Recognize
unstable_useTransitions in v7.10-v7.14, useTransitions from v7.15 onward, and current v8.
The first release reports only when the module contains exactly one proven RouterProvider and it
enables transitions; mixed-provider modules are ambiguous and must be skipped.
Strong positives:
- Block callback with a bare
navigate("/next")statement. - An async callback that invokes but does not await
navigate,submit,fetcher.load, orfetcher.submit. - A branch that returns the promise while another reachable branch drops it.
False-positive traps:
- Expression-bodied
() => navigate(...), which returns the promise. return navigate(...)orawait navigate(...).- Declarative-mode
navigatewhose public return isvoid. - A router without transition opt-in, unless a future v8 release makes the diagnosed behavior unconditional.
- An unrelated
navigateorsubmitfunction.
Implementation boundary:
An autofix from a single bare expression to return is possible only when it cannot change
subsequent callback execution; otherwise suggest the repair.
react-router-no-client-module-in-server-render
Rule definition:
In an SSR/prerendered Framework app, report values imported from .client files or .client/
directories when they are consumed during module initialization or component render.
Runtime reason:
React Router replaces client-module exports with undefined in the server graph. Render-time reads
can crash SSR or produce hydration-divergent markup.
Detector precision: Import-path plus render-path analysis, gated to non-RSC Framework SSR/prerendering.
Strong positives:
- Calling an imported
.clientfunction in the component body. - Reading a
.clientfeature-detection constant to choose rendered markup. - A module-scope call or property access on a client-only import.
False-positive traps:
- Consumption inside
useEffect, a click/change/submit handler,clientLoader, orclientAction. - Passing the function itself as a user-event handler without evaluating it during render.
- SPA mode with no server/prerender graph.
- RSC Framework mode, which uses
"use client"/boundary packages instead of the conventional suffix semantics.
Implementation boundary: No blind move-to-effect autofix; the correct server fallback is product-specific.
react-router-no-redirect-in-try-catch
Rule definition:
Report throw redirect(...) or another proven thrown redirect response inside a try block when the
local catch can swallow or replace it. return redirect(...) remains valid and quiet.
Runtime reason: A thrown redirect is control flow. A broad catch converts it into an error response, log, or fallback, so the navigation never reaches React Router.
Detector precision: Scope-aware thrown-value and catch-path analysis in loaders, actions, and middleware. Data v6.4+ and Framework v7+.
Strong positives:
try { throw redirect("/login") } catch { return data(...) }.- A helper call proven to throw a redirect inside a broad catch that never rethrows route responses.
False-positive traps:
return redirect(...)in the try block.- A catch that recognizes and rethrows the response before handling ordinary errors.
- Redirect construction outside the protected range.
- An unrelated function named
redirect.
Reuse: Generalize the existing Next.js and TanStack Start redirect-in-try-catch control-flow machinery; only the redirect provenance and valid rethrow predicate should differ.
Additional ranked backlog
These candidates have real evidence but either have a narrower payoff, are largely compiler-covered, or need more project analysis before they should be default-on.
| Priority | Candidate | Gate | Contract and boundary |
|---|---|---|---|
| P1 | react-router-server-middleware-return-response |
Framework server middleware, stable >=7.9 or proven unstable opt-in | If server middleware calls next(), every reachable completion must return that Response or an explicit replacement. Client middleware may omit it. |
| P1 | react-router-no-duplicate-route-id |
Data >=6.4 | Report duplicate static explicit IDs across the resolved route tree. Generated, dynamic, and opaque IDs stay unknown. The router throws on collisions. |
| P1 | react-router-no-invalid-splat-path |
all, v6+ | Report static patterns such as files*; React Router warns and interprets them as files/*. Exempt * and valid /*. |
| P1 | react-router-no-nested-router |
all, v6+ | Report an obvious router provider rendered beneath another router in the same resolved tree. Start with direct JSX nesting; do not guess through opaque component composition. |
| P1 | react-router-no-multiple-blockers |
Data >=6.7; stable name >=6.19 | Report two unconditional useBlocker calls in the same component/render path. The router supports one active blocker. Avoid whole-project counting because mutually exclusive routes are valid. |
| P1 | react-router-no-static-cookie-expires |
Framework sessions | Report expires: new Date(Date.now() + ...) in module-scope cookie/session-storage options. The date is fixed once per deployment; recommend maxAge. Do not flag a literal intentional cutoff date. |
| P1 | react-router-no-route-module-environment-suffix |
non-RSC Framework v7+ | Report route files resolved from routes.ts whose filename/directory marks them .server or .client; the Framework build needs route modules in both graphs and fails. |
| P2 | react-router-no-middleware-response-body-consumption |
server middleware >=7.9 | Warn when middleware consumes the Response returned by next() with json, text, formData, blob, or arrayBuffer. Header/status post-processing is supported; body consumption can make the outgoing body unusable. Keep conditional until runtime tests show zero legitimate patterns. |
| P2 | react-router-no-empty-leaf-route |
static UI route tree, v6+ | Report a proven leaf UI route with no element, Component, or lazy; the router warns and renders a null outlet. Exclude resource routes, path prefixes, RSC routes, and intentionally componentless data routes. |
| P2 | react-router-v8-no-react-router-dom-import |
installed v8 | Replace DOM APIs with react-router/dom and other APIs with react-router. Never report this in v6 or v7. |
| P2 | react-router-v8-no-meta-data-field |
installed v8 | Report proven data bindings in meta args and useMatches() results; use loaderData. Scope proof is required because ordinary objects may have data. |
| P2 | react-router-v8-no-removed-future-flags |
installed v8 Framework config | Inspect only the default-exported config. Remove exact future.v8_* flags removed or promoted in v8 plus unstable_previewServerPrerendering; move v8_splitRouteModules to top-level splitRouteModules. |
Non-AST project and dependency diagnostics
These findings belong in React Doctor's project/supply-chain passes, not the oxlint plugin:
| Diagnostic | Version boundary | Why it is not an AST rule |
|---|---|---|
| React Router v8 runtime compatibility | Node >=22.22, React/React DOM >=19.2.7, Framework Vite >=7, ESM-only consumers | Requires package-manager, engine, module-format, and workspace resolution. |
| React Router 7.12 CSRF, redirect XSS, and ScrollRestoration XSS fixes | react-router >=7.0.0 <7.12.0 for CSRF; >=7.0.0 <=7.11.0 for both XSS advisories. The advisories also name affected Remix packages. |
Existing advisory/CVE infrastructure should own semver ranges and remediation. |
| React Router 7.9.6 external-redirect fix | react-router >=6.0.0 <=6.30.1 and >=7.0.0 <=7.9.5; the advisory also covers @remix-run/router <=1.23.1. |
Dependency vulnerability, not a source pattern that can be made safe reliably. |
| React Router 7.9.4 file-session-storage fix | @react-router/node >=7.0.0 <=7.9.3, plus affected Remix Node/Deno packages. Correlate with createFileSessionStorage use. |
Needs dependency and feature-use correlation; avoid a duplicate generic unsigned-cookie lint. |
| React Router 7.9.0 meta XSS fix | react-router >=7.0.0 <=7.8.2; the advisory also covers @remix-run/react >=1.15.0 <=2.17.0. |
A safe source rewrite is not equivalent to the upstream escaping fix. |
| React Router 7.5.2 cache/pre-render fixes | react-router >=7.2.0 <=7.5.1 for forced-SPA cache poisoning and >=7.0.0 <=7.5.1 for pre-render data spoofing. |
These are server-runtime package flaws, not author mistakes. |
| React Router Express 7.4.1 host validation fix | @react-router/express 7.0.0-7.4.0; the advisory also names affected @remix-run/express versions. |
Adapter dependency flaw, not a route source pattern. |
These are the exact affected ranges returned by the repository's public GitHub security-advisory records on the research date. The supply-chain implementation should ingest those canonical records instead of maintaining a second hand-written range table; the rows above are review evidence, not a new advisory database.
Reuse and deduplication findings
| Proposed behavior | Existing code | Decision |
|---|---|---|
| Navigate render-path analysis | tanstack-start-no-navigate-in-render |
Extract/share the hardened detector. |
| Independent loader awaits | tanstack-start-loader-parallel-fetch |
Generalize the recognizer; do not copy the dependency analysis. |
| Thrown redirect inside catch | nextjs-no-redirect-in-try-catch and tanstack-start-redirect-in-try-catch |
Share catch/rethrow analysis; add React Router redirect provenance. |
| Request-body consumption | request-body-mass-assignment has body-parser provenance, but a different security purpose |
Reuse request-binding utilities only; do not broaden the mass-assignment rule. |
| Reachable duplicate continuation calls | existing path/reachability utilities used by state/effect rules | Reuse control-flow primitives for middleware next; do not invent middleware-specific traversal. |
| Session/cookie provenance | generic session-cookie security constants and scanners | Reuse names only after proving the React Router storage/session source; generic naming alone is insufficient. |
| Client-only render paths | existing SSR/browser-global and effect/event-handler recognizers | Reuse render/effect boundary analysis for .client imports. |
| Internal anchor syntax filters | tanstack-start-no-anchor-element |
Reuse only after adding React Router route-target proof. |
| Fetch in effects | generic no-fetch-in-effect |
Do not add a blanket React Router duplicate. |
| Unsafe URL redirect | clickjacking-redirect-risk and url-prefilled-privileged-action |
Improve the generic security detector if needed; do not add a router-branded duplicate. |
| Route/special filenames | is-framework-route-or-special-filename already accepts react-router and remix runtimes |
Extend its route-module coverage after mode detection. |
| Import provenance | getImportedNameFromModule / isImportedFromModule |
Reuse for aliases and package variants. |
| Cross-file component tracing | existing cross-file probe/dependency utilities and Next rules | Reuse for Outlet and root-boundary project checks. |
| Fast Refresh route exports | only-export-components tables already know data-router creators and route exports |
Keep; no new rule. |
Pre- and post-research truffler searches found no existing React Router implementation for the
candidate names or behaviors. Exact source searches found the Next.js/TanStack redirect detectors,
TanStack route detectors, request-body security analysis, and shared import, reachability, SSR, and
filename utilities above. The empty truffler results for several behavior phrases are not proof
of absence on their own; the registry and filename/source searches provide the confirming audit.
Daytona pull-request field audit
The first pushed candidate was evaluated against 2,000 pinned open-source projects. Daytona produced 1,989 baseline reports and 1,981 candidate reports before the shared 18-minute scan budget; all missing records were explicit time-budget failures, not malformed output or unpinned refs. The candidate reports contained 55 React Router diagnostics across 36 repositories.
Pinned-source review confirmed the router-factory, render-phase navigation, root error boundary, route-object, route-lazy, and loader abort-signal findings. It also exposed three detector boundaries that were too broad:
- Imported
.clientcomponents rendered inside an importedClientOnlyrender prop are already excluded from the server graph. Six findings infrontvibe/fluiddemonstrated this shape. - A default-exported
.clienthelper nested inside a route folder is not itself a route module. The actualroute.tsxinfrontvibe/fluiddynamically imported that helper insideClientOnly. - Mutating the stable
useSearchParams()value can still synchronize navigation through an enclosing setter, a returned serialized URL, or a provenuseNavigate()call. Six findings inapache/answerandYooooomi/your_spotifydemonstrated those data-flow shapes.
The shipped detectors now abstain on those proven boundaries, and each source shape is preserved as
a regression test. The .client JSX rule requires an imported client-only render-prop boundary;
the route-suffix rule only treats direct files under routes/ as statically proven flat route
modules; and the search-param rule follows inline iterator ownership plus returned or navigated
serialization. These exits deliberately trade recall for evidence-backed precision.
Rejected or deferred ideas
- Blanket
no-fetch-in-effectfor route files: already covered generically, and client-only or background data can be legitimate even in a route component. - Require
HydrateFallbackwheneverclientLoader.hydrate = true: official docs allow no fallback when server-rendered and client-loaded initial data are identical. Static proof of a mismatch is too weak for a default-on rule. - Require an error boundary on every data route: boundaries intentionally protect descendant routes. Only root coverage is universal.
- Ban
<Navigate>or all navigation in effects:<Navigate>remains necessary in Declarative or descendantRoutes, anduseNavigateexplicitly supports effects. A preference for loader redirect requires route/mode and intent evidence that a per-file rule usually lacks. - Ban
navigate(-1): the docs caution about missing or cross-domain history entries but also document valid modal and wizard uses. Intent is not statically knowable. - Always use
Formor always usefetcher.Form: the correct choice depends on whether the URL and history should change. Markup alone rarely proves that product intent. - Require
prefetch="intent"on every Link: prefetching trades latency for bandwidth and server load; it is not universally beneficial. - Require custom
shouldRevalidateto calldefaultShouldRevalidate: overriding the default is the purpose of the API and can be intentional. An unconditionalfalsecan still be correct for immutable data. - Require status 400 for every returned validation-shaped object: the object's semantic role cannot be inferred reliably, and 200 responses can be deliberate.
- Require only GET/POST on every
Form: React Router deliberately supports PUT/PATCH/DELETE. Native-form compatibility is a product requirement, not a universal correctness contract. - Require cookie secrets on every cookie/session: unsigned preference cookies are legitimate. Correlating an unsigned cookie with auth-sensitive fields may be useful security research, but a blanket rule would be noisy and the known file-session vulnerability belongs in dependency scanning.
- Ban response-body reads in all middleware immediately: the docs steer server middleware to status/header changes, but some advanced response transformations may be intentional. Keep the narrowly proven body-consumption candidate conditional until open-source evaluation establishes its precision.
- Require every server middleware to call
next(): omittingnextintentionally short-circuits the chain, and React Router automatically proceeds when middleware is absent. Only diagnose the response-return contract afternextis actually called. - Ban native forms or internal anchors syntactically: full-document navigation, no-JS behavior, resource routes, downloads, and non-router endpoints are all valid. Only project-resolved targets make these checks sound.
- Flag awaited loader promises as non-streaming: the linter cannot know which data is critical to first render. React Router supports both awaited critical data and returned streaming promises.
- Lint unstable RSC APIs now: the official skill marks RSC support unstable. Wait for stable contracts or gate an experimental ruleset explicitly.
Suggested implementation order
- Add React Router presence, mode, installed-version, target-major, SSR/RSC, and package-source capabilities. Treat unknown versions conservatively.
- Ship the syntax/scope P0 rules with small surfaces: loader request body, session mutation in a
loader, multiple middleware
next, loader data in proven error UI, and invalid lazy properties. - Generalize the existing TanStack render-path/loader-waterfall and cross-framework redirect-in-catch detectors instead of cloning them.
- Add Framework path recognition for root, route, config, client/server-only, and server entry
modules. Then ship root error coverage, aborted
handleError, static cookie expiry, and route module suffix checks. - Build the conservative route/component index and validate it first with missing
Outlet, descendantRouteswithout splat, duplicate IDs, invalid absolute children, and resource links. - Add v8 migration diagnostics only behind an explicit target-major setting; switch them to errors when v8 is installed. Keep runtime/dependency compatibility in the project scanner.
- Run focused tests, fuzzing, RDE open-source evaluation, and pull-request parity for each rule separately. Test every version gate with fixtures at the boundary minor, not just one v6/v7/v8 sample. Do not ship project-aware rules until unresolved targets produce zero reports.
The first implementation PR should stay narrow: mode/version foundation plus
react-router-no-loader-request-body and react-router-no-multiple-middleware-next. Both have
direct primary evidence, compact detectors, hard version gates, and no route-index dependency. The
route index is valuable, but coupling it to the first rule would make review and false-positive
attribution unnecessarily difficult.