solid-router 2.0.0-rc.0: any useRouterState call desyncs hydration of later siblings (unclaimed dead DOM)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.1k
- Forks
- 1.9k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 143
Description
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 https://github.com/solidjs/solid/issues/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;
};
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the useRouterState implementation and the minimal SolidStart reproduction in the issue, then compare it with solid-js issue #3012. Done means a supported change or upstream fix prevents hydration tag mismatches and leaves later sibling DOM claimed; verify the checkbox interaction and absence of unclaimed-node warnings.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100