Skip to content

fix(react-router): a route that renders <Outlet /> renders nothing of its own - #2670

Merged
colbymchenry merged 2 commits into
mainfrom
claude/route-outlet-renders-nothing
Oct 19, 2026
Merged

colbymchenry merged 2 commits into
mainfrom
claude/route-outlet-renders-nothing

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Follow-up 1 of #2668.

Problem

A React Router route with no element renders an <Outlet />. JSX routes already read element={<Outlet />} as rendering nothing of their own. Route objects did not:

  • element: <Outlet /> and Component: Outlet made Outlet the route's page and the layout of every route inside it. Those layout:Outlet references link nothing. On current main they are 93 failed rows on Medusa's admin dashboard, 106 on Mercur's admin panel and 87 on mercurjs/vendor-panel.
  • A lazy loader returning { Component: Outlet, loader }. Medusa's :id routes import their page module only for its loader and breadcrumb. The loader reader (fix(react-router): a lazy route links the page its loader picks #2480) couldn't read that, so the route fell back to the module the loader imports. That module's page became the layout of the routes inside it: /products/:id/variants/:variant_id and its five children were drawn inside ProductDetail. That made 13 wrong layout edges on Medusa and 5 on each Mercur panel, the extras fix(react-router): read the table a function returns when a variable hands it to the router #2668's oracle flagged.

Fix (frameworks/react.ts)

  • A route object that renders Outlet (element: <Outlet />, Component: Outlet, or a loader handing back { Component: Outlet } or { element: <Outlet /> }) becomes an outlet scope:
    • It stays a route at its own address, inside the layouts around it, so a navigation there still lands. nomik-oss, for one, writes a dozen routes like { path: 'approvals', element: <Outlet /> } that its shell draws itself. A first version dropped these routes, and the corpus A/B below caught it.
    • It has no page reference and is no layout.
    • A page inside at the same address claims it, as before ({ path: 'regions', element: <Outlet />, children: [{ path: '', … }] }).
    • At its parent's address (path: '') it only groups its children. React Router's error-boundaries example writes this to hold an errorElement, and the parent stays the page there. JSX already treats <Route path="" element={<Outlet />}> this way.
  • lazyRouteReference returns Outlet for a loader that hands it back. Before, that loader fell back to its first import. The JSX branch drops Outlet the way it drops element={<Outlet />}.

Verification

Regression test. __tests__/react-router-outlet-routes.test.ts covers Medusa's shapes (element: <Outlet /> and Component: Outlet parents, :id loaders returning Component: Outlet and element: <Outlet />), addresses only the shell draws, a navigation to each, and the error-boundaries shape:

  • On current main (654a853f), 2 of its 3 tests fail: the wrong layouts, and the leftover Outlet route refs.
  • The third (an outlet route stays an address and keeps its navigation) passes on main. It guards against dropping such routes.
  • Mutations, each restored afterwards:
    • letting route objects keep Outlet as their component fails 1 test;
    • not counting outlet routes as pages fails 2;
    • not reading the loader's Outlet fails 1;
    • making a path: '' outlet a route of its own fails 2.

Checks. tsc --noEmit is clean, and the React Router and React test files pass. Full suite with the kernel built and CODEGRAPH_KERNEL_EXPECT=1, --maxWorkers=4: 7,479 passed, 0 failed, 80 skipped, on 654a853f plus this branch. After syncing #2669 (3695818f, a sync-time import retry): tsc, 11 React Router and sync-import test files (178 tests), and a re-index of Medusa, both Mercur panels, rr-examples and outline with the synced build, byte-identical to the pre-sync dumps.

A/B on real repos

Same kit as #2668: fresh codegraph init with the kernel, current main 654a853f against this branch; only react.js differs.

repo change
medusajs/medusa packages/admin/dashboard −13 layout edges (ProductDetail, ProductOptionDetail and TaxRegionDetail around their :id route's other children), 6 more re-attributed to the '' child that really renders TaxRegionDetail (same edge, new refName), all 93 layout:Outlet rows gone
mercurjs/admin-panel −5 layout edges (the same ProductDetail around the variant routes), all 106 layout:Outlet rows gone
mercurjs/vendor-panel −5 layout edges (the same), all 87 layout:Outlet rows gone
remix-run/react-router examples/error-boundaries / is the Layout route linked to Layout, instead of the path: '' child linked to React Router's own Outlet function; its 1 layout:Outlet row gone
21 other repos dumps byte-identical
  • Oracle. fix(react-router): read the table a function returns when a variable hands it to the router #2668's independent route oracle (the TypeScript compiler, React Router's path rules, barrels followed by hand) now agrees exactly on both dashboards. Medusa has 821 of 821 expected edges with no extras (13 extras before); Mercur has 775 of 775 (5 extras before).
  • Survey. Running both builds' extract() over 2,677 distinct router files from four GitHub code-search corpora changes 10 files. It only drops 40 references: 24 layout:Outlet and 16 Outlet page refs. No route is added or removed.

🤖 Generated with Claude Code

colbymchenry and others added 2 commits October 19, 2026 12:45
…of its own

A route with no element renders an `<Outlet />`, and JSX routes already read
`element={<Outlet />}` that way. Route objects did not:

- `element: <Outlet />` and `Component: Outlet` made `Outlet` the route's
  page and the layout of every route inside it. Those `layout:Outlet`
  references link nothing: 93 failed rows on Medusa's admin dashboard, 106
  on Mercur's, 87 on mercurjs/vendor-panel.
- A `lazy` loader returning `{ Component: Outlet, loader }` (Medusa's `:id`
  routes import their page module only for its loader and breadcrumb) read
  as unreadable, so the route fell back to that module's page, which became
  the layout of the routes inside: a product's detail page drawn around its
  variants' pages.

Such a route object is now an `outlet` scope. It is a route at its own
address, so a navigation there still lands, and it sits inside the layouts
around it, but it has no page reference and is no layout. A page inside at
the same address claims the address as before, and a `path: ''` one only
groups its children at its parent's address. `lazyRouteReference` returns
`Outlet` for a loader that hands it back, and the JSX branch drops it the way
it drops `element={<Outlet />}`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant