TanStack / TanStack/router

solid-router 2.0.0-rc.0: any useRouterState call desyncs hydration of later siblings (unclaimed dead DOM)

Open
#8,096 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

information needed
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.