On @tanstack/solid-router@2.0.0-rc.0 + solid-js@2.0.0-rc.0, a component that calls useRouterState({ select }) and has later siblings in its parent leaves those siblings unclaimed after hydration ("Hydration tag mismatch", "Hydration completed with N unclaimed server-rendered node(s)") — their SSR DOM stays visible but dead. A selector that returns a constant and is never read still triggers it: creating the memo is enough.
Root cause is in solid-js (filed as solidjs/solid#3012): useRouterState creates its select memo with the internal option { transparent: true }, and a transparent memo created during hydration shifts sibling hydration ids. Until solid-js fixes that (or useRouterState stops passing transparent), every solid-start app hits this the moment a useRouterState consumer isn't the last child of its parent.
Reproduction
Minimal solid-start reproduction (~60 lines): a route component rendering <main> + a footer that calls useRouterState + a <pre> sibling — the <pre> fails to claim:
import { createFileRoute, useRouterState } from '@tanstack/solid-router';
import { createSignal } from 'solid-js';
export const Route = createFileRoute('/')({ component: Page });
function Footer(props: { disabled: boolean }) {
const navigationPending = useRouterState({
select: (state) => {
const resolved = state.resolvedLocation?.pathname;
if (resolved === undefined) return false;
return resolved !== state.location.pathname;
},
});
return (
<nav>
<button id="continue" disabled={props.disabled || navigationPending()}>
Continue
</button>
</nav>
);
}
function Page() {
const [checked, setChecked] = createSignal(false);
return (
<>
<main>
<label>
<input
type="checkbox"
checked={checked()}
onChange={(e) => setChecked(e.currentTarget.checked)}
/>{' '}
done
</label>
</main>
<Footer disabled={!checked()} />
<pre id="debug">debug: {String(checked())}</pre>
</>
);
}
Result on load: Hydration tag mismatch ... expected <pre> + Hydration completed with 1 unclaimed server-rendered node(s): <pre id="debug">... and the checkbox never enables the Continue button. Replace useRouterState with a plain accessor (below) or a constant and hydration is clean. A pure-solid reproduction with no TanStack packages (a bare createMemo(() => false, { transparent: true }) in the footer) fails identically — see the solid-js issue.
Workaround we ship meanwhile
Read the stores through a plain accessor instead of useRouterState — identical reactivity (the underlying stores are signals on the client), no memo creation during hydration:
const router = useRouter();
const navigationPending = () => {
const resolved = router.stores.resolvedLocation.get()?.pathname;
if (resolved === undefined) return false;
return resolved !== router.stores.location.get().pathname;
};
On
@tanstack/solid-router@2.0.0-rc.0+solid-js@2.0.0-rc.0, a component that callsuseRouterState({ select })and has later siblings in its parent leaves those siblings unclaimed after hydration ("Hydration tag mismatch", "Hydration completed with N unclaimed server-rendered node(s)") — their SSR DOM stays visible but dead. A selector that returns a constant and is never read still triggers it: creating the memo is enough.Root cause is in solid-js (filed as solidjs/solid#3012):
useRouterStatecreates its select memo with the internal option{ transparent: true }, and a transparent memo created during hydration shifts sibling hydration ids. Until solid-js fixes that (oruseRouterStatestops passingtransparent), every solid-start app hits this the moment auseRouterStateconsumer isn't the last child of its parent.Reproduction
Minimal solid-start reproduction (~60 lines): a route component rendering
<main>+ a footer that callsuseRouterState+ a<pre>sibling — the<pre>fails to claim:Result on load:
Hydration tag mismatch ... expected <pre>+Hydration completed with 1 unclaimed server-rendered node(s): <pre id="debug">...and the checkbox never enables the Continue button. ReplaceuseRouterStatewith a plain accessor (below) or a constant and hydration is clean. A pure-solid reproduction with no TanStack packages (a barecreateMemo(() => false, { transparent: true })in the footer) fails identically — see the solid-js issue.Workaround we ship meanwhile
Read the stores through a plain accessor instead of
useRouterState— identical reactivity (the underlying stores are signals on the client), no memo creation during hydration: