Which project does this relate to? Router
Describe the bug
In @tanstack/solid-router, a route whose loader throws gets its errorComponent instantiated at two different points in the component tree depending on the side. MatchInner's status === 'error' branch (packages/solid-router/src/Match.tsx) renders the error component inline on the server, as a child of its own <Solid.Match>, but throws on the client, so CatchBoundary's <Solid.Errored> catches it and renders the error component from its fallback, several levels higher:
<Solid.Match when={currentMatch().status === 'error'}>
{(_) => {
if (isServer ?? router.isServer) {
const RouteErrorComponent =
(route.options.errorComponent ?? router.options.defaultErrorComponent) || ErrorComponent
return <RouteErrorComponent error={currentMatch().error} info={{ componentStack: '' }} />
}
throw currentMatch().error
}}
</Solid.Match>
Solid hydration is id-addressed, and the two sides mint the error component's ids under different namespaces — server: Match → CatchBoundary → Errored → Show → RouteContent → … → MatchInner → Show → Switch → Match; client: Match → CatchBoundary → Errored. The client's root element therefore names a key no server element carries. @solidjs/web logs Hydration key miss and builds the client copy detached; the server's copy stays in the document, unclaimed, with the markup the user sees and no event listeners. Any control inside an error component is dead after hydration. A one-element error component (a bare <h1>) looks fine, because nothing in it needed wiring — which is how this stays invisible until someone puts a button in one.
From the reproducer, the server id, the client's missed claim, and — for comparison — the id the same component gets when rendered from the route's normal component:
server …0004900010002 1011032370000 ← MatchInner renders <RouteErrorComponent> inline
client …0004900010002 200100 ← <Errored fallback> instantiates it at the CatchBoundary
control …0004900010002 1032371102 ← MatchInner's success branch
The React router has the same server/client split and gets away with it because React reconciles by tree position and re-renders on a mismatch; Solid does neither.
Complete minimal reproducer
https://github.com/TylerRick/tanstack-solid-router-error-component-hydration-repro
Steps to Reproduce the Bug
pnpm install && pnpm dev, then npx playwright install chromium once and node check.mjs (or open http://localhost:5602 and click the button by hand).
/ is the subject: the loader throws synchronously; the route's errorComponent renders a <Card> component — a heading, a #count span, and a #bump button that increments it. /control renders the identical <Card> from the route's component. (/show, /async, and /element vary the error component's root and the loader's timing; they behave the same.)
check.mjs loads each route, collects console output, and clicks the visible #bump:
--- SUBJECT (component-rooted errorComponent)
hydration messages: 2
[warning] Hydration key miss for "0000010101149a001000210323711620004900010002200100": no server-rendered
element carries this key (template: <section id=card …). A detached element was created instead…
[warning] Hydration completed with 1 unclaimed server-rendered node(s):
<section _hk="0000010101149a0010002103237116200049000100021011032370000" id="card" …
#count after clicking the visible #bump: ["0"]
visible button wired: false
--- CONTROL (same Card via the route component)
hydration messages: 0
#count after clicking the visible #bump: ["1"]
visible button wired: true
Expected behavior
The error component hydrates: no key miss, and its controls work after hydration — the same as the identical component rendered from component. That needs the error component to be instantiated from ONE place on both sides; whether that is the server also throwing into <Errored>, or the client rendering inline as the server does (with reset passed through context rather than the boundary), is your call.
Screenshots or Videos
n/a — check.mjs output above.
Platform
@tanstack/solid-router 2.0.0-rc.4, @tanstack/solid-start 2.0.0-rc.4, @tanstack/router-core 1.171.22 — the Match.tsx branch quoted above is unchanged on main today
solid-js / @solidjs/web 2.0.0-rc.6
- OS: Linux
- Browser: Chromium (Playwright 1.62.1), 151.0.7922.34
- Bundler: Vite 8.2.2,
vite-plugin-solid 3.0.0-next.27
Additional context
Found in a TanStack Start app while giving a route an errorComponent with a "sign out and retry" button for its 403: the button rendered and did nothing. The app's default error component is a lone <h1>, which is why the key miss had been sitting in our logs as harmless. In that app the detached client copy was also inserted into the page next to the server's (the error UI rendered twice); the reproducer shows the key miss and the dead button but holds one copy, so I have not isolated what the duplicate needs.
Which project does this relate to? Router
Describe the bug
In
@tanstack/solid-router, a route whose loader throws gets itserrorComponentinstantiated at two different points in the component tree depending on the side.MatchInner'sstatus === 'error'branch (packages/solid-router/src/Match.tsx) renders the error component inline on the server, as a child of its own<Solid.Match>, but throws on the client, soCatchBoundary's<Solid.Errored>catches it and renders the error component from itsfallback, several levels higher:Solid hydration is id-addressed, and the two sides mint the error component's ids under different namespaces — server:
Match → CatchBoundary → Errored → Show → RouteContent → … → MatchInner → Show → Switch → Match; client:Match → CatchBoundary → Errored. The client's root element therefore names a key no server element carries.@solidjs/weblogsHydration key missand builds the client copy detached; the server's copy stays in the document, unclaimed, with the markup the user sees and no event listeners. Any control inside an error component is dead after hydration. A one-element error component (a bare<h1>) looks fine, because nothing in it needed wiring — which is how this stays invisible until someone puts a button in one.From the reproducer, the server id, the client's missed claim, and — for comparison — the id the same component gets when rendered from the route's normal
component:The React router has the same server/client split and gets away with it because React reconciles by tree position and re-renders on a mismatch; Solid does neither.
Complete minimal reproducer
https://github.com/TylerRick/tanstack-solid-router-error-component-hydration-repro
Steps to Reproduce the Bug
pnpm install && pnpm dev, thennpx playwright install chromiumonce andnode check.mjs(or openhttp://localhost:5602and click the button by hand)./is the subject: the loader throws synchronously; the route'serrorComponentrenders a<Card>component — a heading, a#countspan, and a#bumpbutton that increments it./controlrenders the identical<Card>from the route'scomponent. (/show,/async, and/elementvary the error component's root and the loader's timing; they behave the same.)check.mjsloads each route, collects console output, and clicks the visible#bump:Expected behavior
The error component hydrates: no key miss, and its controls work after hydration — the same as the identical component rendered from
component. That needs the error component to be instantiated from ONE place on both sides; whether that is the server also throwing into<Errored>, or the client rendering inline as the server does (withresetpassed through context rather than the boundary), is your call.Screenshots or Videos
n/a —
check.mjsoutput above.Platform
@tanstack/solid-router2.0.0-rc.4,@tanstack/solid-start2.0.0-rc.4,@tanstack/router-core1.171.22 — theMatch.tsxbranch quoted above is unchanged onmaintodaysolid-js/@solidjs/web2.0.0-rc.6vite-plugin-solid3.0.0-next.27Additional context
Found in a TanStack Start app while giving a route an
errorComponentwith a "sign out and retry" button for its 403: the button rendered and did nothing. The app's default error component is a lone<h1>, which is why the key miss had been sitting in our logs as harmless. In that app the detached client copy was also inserted into the page next to the server's (the error UI rendered twice); the reproducer shows the key miss and the dead button but holds one copy, so I have not isolated what the duplicate needs.