Repository navigation
fix(react-router): a JSX index route is the page at its parent's address, inside the layout around it - #2423
Merged
Merged
Conversation
…ess, inside the layout around it
Route objects already read `{ index: true, element }` as the page at the
parent's address and a route object around others as their layout. JSX
routes did neither: `<Route path="/" element={<Layout/>}><Route index
element={<Home/>}/>…</Route>` bound `/` to Layout, the index child was never
read, and Layout's links counted only from `/`.
The JSX branch of scanRoutes now:
- reads `<Route index>` / `index={true}` (and a `path=""` leaf with an
element) as the page at its parent's address, and lets it claim that
address from the layout or the path-only `<Route path>` around it;
- treats a `<Route element>` with `<Route>`s inside as their layout
(`layout:` references), a pathless or `path=""` one included, and threads
those layouts into a table mapped inside it when the table is written in
the same file;
- reads an index route only where its parent's address is written down: an
enclosing route with a readable path, or the router itself
(`createRoutesFromElements(…)`, `<BrowserRouter>`). At the top of a
component's own `<Routes>` the address is wherever another file mounts the
component, and under `path={paths.x}` it is not spelled out;
- reads `path={"agents"}` as a path.
Reading what an element renders: tags inside an attribute are no longer the
page unless the attribute hands one over (`component`, `element`, `page`),
so `<Suspense fallback={<Loader/>}>` no longer binds the spinner; the line
break Prettier writes after `element={` no longer hides the element; a guard
that wraps only `<Outlet/>` is the guard; and `element={<Outlet/>}` renders
nothing of its own.
Validated against the merged #2400 build: 16 control repos byte-identical,
578 of 581 new layout edges name a component of an enclosing <Route> (3
inherit a pre-existing default-import resolution bug), 27 of 27 new
navigations precise, no navigation lost.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…490d4 # Conflicts: # CHANGELOG.md
This was referenced Oct 7, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
#2400 taught route objects that
{ index: true, element }is the page at its parent's address and that a route object around others is their layout. JSX routes still did neither:<Route path="/dashboard"><Route index element={<DashboardHome/>}/></Route>gave a/dashboardroute with no binding. The index child was never read.<Route path="/" element={<Layout/>}><Route index element={<Home/>}/><Route path="about" …/></Route>, the classic v6 shape, bound/to Layout instead of Home. Layout's header links counted only from/, and a guard like<Route element={<RequireAuth/>}>was invisible.Fix
In the JSX branch of
scanRoutes(frameworks/react.ts):<Route index>/index={true}with no path (orpath="") is the page at its parent's address. The "a child that claims the address is the page there" rule now applies to JSX too. A layout or a path-only<Route path>whose address a child claims is not a page there, so one address still has one route.<Route element>(or v3-stylecomponent=) with<Route>s inside is their layout. Each nested route gets alayout:reference, whichrouteLayoutsalready draws on Screens. This includes a pathless orpath=""guard like<Route element={<RequireAuth/>}>, which is a layout at no address of its own.<Route path>with no element is a route even with nothing to render, unless a child claims its address. It is never a layout..mapped inside a<Route element>gets that layout when the table is written in the same file. chickadeeinvest'sDashboardRoutesis the example.Where an index route's address is written down. An index route's address is its parent's. When no route around it spells that out, it is not read:
<Routes>, the parent is wherever another file mounts the component. crwn-clothing'sShopis mounted atshop/*, so its index route is/shop, not a second home page. A top-level index route counts as/only inside the router itself (createRoutesFromElements(…),<BrowserRouter>…).path={paths.agents}the address is not spelled out either.path={"agents"}is, and is now read as a path.What an element renders. The same reader picks a route's page and its layout, so two existing misreads would have spread into layouts:
component,element,page). Before,<Suspense fallback={<Loader/>}><AdminPanel/></Suspense>bound 15 routes of one app to the spinner.element={no longer hides the element. Every multi-line element used to bind to nothing.AuthPagematches theAuth…guard rule), the innermost one is the page rather than the outermost.<Outlet/>is the guard, andelement={<Outlet/>}renders nothing of its own.<Route path="" component={NotFound}/>is still a catch-all, not a page at/.Validation
mainat31c3328d(#2400 included) against this branch, each repo indexed fresh (init -y) and compared withscripts/dump-graph.mjs. Both arms' dumps are byte-identical to the same comparison against #2400's own commit, so the PRs merged since don't touch these repos.path={"agents"})path=""at the root)react-router@6.29.0(examples + tests)examples/(281 apps)Precision, site by site:
element(orcomponent) of a<Route>still open where the route's own tag starts, or around the.mapof its table. 578 of 581 pass. vanlife's three fail because of an existing import bug, not this change:import HostVanDetail, { loader as … } from './HostVanDetail'resolves the default import to the module'sloader.mainalready binds five of that app's pages toloaderoraction(/vans,/login, …). The new layout references go through the same import resolution, so they inherit it. This is the standard data-router module shape, so it is filed separately.navigatesedges name their destination at the line they come from: chushi 4, hathor-explorer 2, refine 21.navigatesedge is still there, at a route of the same name. 48 of them now reach the route that claims that address, such as proshop-v2's four navigations to/, which reach HomeScreen instead of App.element={<Outlet/>}routes in React Router's own tests no longer bind to itsOutlet. That repository definesOutletitself; in an app,Outletis a package import that resolves to nothing either way. The other 60 binding changes are routes going from unbound to bound, all from the element reader. Most are multi-line elements; the rest are aSuspensewith afallback, orRequireAutharound the page.Survey. 428 files came from GitHub code searches for JSX index and layout routes; 386 are not tests. Run through
extract()on both arms:path=""child.Suspensefallback.I read every removed and added route, which is how the descendant-
<Routes>,path={expr}and fallback cases above were found and fixed.Screens (
buildScreens): every repo above stays routed./is HomeScreen, crwn-clothing's/is Home, and cboard's/editor/:idis Editor./loginis drawn from the screens it guards. In proshop-v2,PrivateRouteandAdminRoutewere unattributed origins before.Some base transitions are drawn differently, none dropped:
usePublicOrPrivateNavigatenow reach more than three screens, so Screens' existing shared-chrome rule draws them once as an origin.<Link to="/">in finefoods-antd'sTitle, hits the attribution walk's 800-node cap in that 281-app monorepo.Sync converges. On a copy of proshop-v2, I edited its route file four times and ran
codegraph syncafter each edit: dropping the index route, restoring it, swapping apath=''guard layout, and restoring that. Each synced graph is byte-identical to a freshinit -yof the same files.Tests.
__tests__/react-router.test.tsgains:createRoutesFromElementswithpath=''guards.Auth…page, andrememberMe={<…/>}.<Routes>,path={paths.x},path={"x"}, and an index route at the top of<BrowserRouter>and ofcreateRoutesFromElements./dashboardto DashboardHome.<Route element={<DashboardLayout/>}>now has its layout edges. A table imported from another file and mapped under a layout is pinned as getting none.react.ts, 18 of these fail. A 19th, fix(react-router): read route tables another file hands the router #2400's own two-sync table test, also failed in that run by timing out at 6.2 s under load; it passes on this branch.tscis clean, and all 72 tests in the two React Router test files pass.function-ref's Python-only Caller/impact graph misses methods passed as first-class references (callbacks), e.g. executor.submit(obj.method, …) #1820 case. It hits its own 60 s timeout even alone at that load, and it indexes no React file.Known limits
<Routes>is not read, because its address is wherever another file mounts the component. That includes anApp.jsxwhose<Routes>is the app's root while<BrowserRouter>sits inmain.jsx. Its paths, read as before, still compose from the root. Composing descendant routes under their mount is the cross-file follow-up fix(react-router): read route tables another file hands the router #2400 already notes.<Route element>gets no layout edge. The layout's name would be looked up in the table's file. Also, the cross-file pass compares only route names, so a changed layout would not reach the table's routes on sync.ThemedLayout) and index pages (NavigateToResource) from refine's own packages. Those references fail, which accounts for the +1249 refs. Base already leaves the same kind of failed reference for any route whose page comes from a package.export default function X()resolves to that earlier function. This existing import-resolver bug misbinds data-router pages and the layouts that use them; it is filed as a separate task.Overlap
#2420 (open) touches the same files in these places:
docs/design/framework-coverage.md, and edits the same React Router row and "Last verified" line.__tests__/react-router.test.tsa few lines above this PR's same-file-table test.Whichever lands second renumbers its trap to 15 and merges the row. The code doesn't overlap: #2420 changes how a JSX name binds in
react.ts'sresolve(), and this PR changes the route scanner and the element reader.Docs
docs/design/framework-coverage.md.[Unreleased]→### Fixes.docs/viewer-launch-changelog.md.🤖 Generated with Claude Code