Summary
On a TanStack Start storefront running on Cloudflare Workers (React 19.2.7, production build), the home page logs Uncaught Error: Minified React error #418 (Hydration failed because the server rendered HTML didn't match the client) during hydration, and React discards the whole server-rendered tree under main > div.
I verified the side effect by tagging the SSR nodes before hydration and checking node identity afterwards: every child of main > div (h1.sr-only, section#Theme-Theme, #Header-Header, #Images-Carousel, the shelves, #Footer-Footer, …) gets detached and re-created on the client, and the useId values change from _R_ikn6_ / _R_kikn6_ to _r_0_ / _r_1_. In practice the whole home is rendered twice and the SSR benefit is lost (extra main-thread work, visual flash, LCP/INP impact).
Reproduction
- 3/3 reproductions with a cold browser cache (first loads of the origin).
- 0/4 reproductions with a warm cache (JS assets already cached).
The error is not visible with a naive console read because it happens during hydration; I captured it by hooking console.error and the error event before any page script runs.
Evidence that it is a transient (race) mismatch, not a static markup difference
- Comparing the pre-hydration DOM with the client-regenerated tree, the structure is identical — the only differences are
useId values and style attribute serialization (border-bottom-color:#FFFFFF vs border-bottom-color: rgb(255, 255, 255)).
- The server HTML for the home contains 16 skeleton placeholders (
animate-pulse), i.e. the page relies heavily on deferred sections.
main's server HTML is ~68.6 KB and matches the regenerated client tree, so nothing structural is missing/extra in a stable way.
Suspected root cause
In packages/tanstack/src/hooks/DecoPageRenderer.tsx:
- Lines 258-259 seed the very first client render from the module-level deferred cache:
const [section, setSection] = useState<ResolvedSection | null>(() =>
typeof document === "undefined" ? null : getCachedDeferredSection(stableKey),
);
On the server this is always null (skeleton is emitted), while on the client the first render — the hydration render — can already return resolved content. Any divergence here is a guaranteed hydration mismatch.
- The streamed path at lines 539-557 (
<Suspense fallback={skeleton}><Await promise={promise}>…) can have its data resolved before hydration starts on slow/cold loads, which is consistent with the cold-cache-only reproduction: when the JS bundles are already cached, hydration wins the race and no error is reported.
Suggested fix
Keep the first client render byte-identical to the SSR output and only adopt resolved/cached content in a later commit:
- read
getCachedDeferredSection() inside useEffect (after mount) instead of in the useState initializer, or
- use
useSyncExternalStore with getServerSnapshot() returning null so server and hydration snapshots always agree, or
- gate the cache read behind an explicit
hydrated flag set after the first commit.
Related
I could not determine the exact @decocms/start version of this deployment (no version endpoint exposed), so it would be useful to know whether this is residual behaviour from something already fixed.
Summary
On a TanStack Start storefront running on Cloudflare Workers (React 19.2.7, production build), the home page logs
Uncaught Error: Minified React error #418(Hydration failed because the server rendered HTML didn't match the client) during hydration, and React discards the whole server-rendered tree undermain > div.I verified the side effect by tagging the SSR nodes before hydration and checking node identity afterwards: every child of
main > div(h1.sr-only,section#Theme-Theme,#Header-Header,#Images-Carousel, the shelves,#Footer-Footer, …) gets detached and re-created on the client, and theuseIdvalues change from_R_ikn6_/_R_kikn6_to_r_0_/_r_1_. In practice the whole home is rendered twice and the SSR benefit is lost (extra main-thread work, visual flash, LCP/INP impact).Reproduction
The error is not visible with a naive console read because it happens during hydration; I captured it by hooking
console.errorand theerrorevent before any page script runs.Evidence that it is a transient (race) mismatch, not a static markup difference
useIdvalues andstyleattribute serialization (border-bottom-color:#FFFFFFvsborder-bottom-color: rgb(255, 255, 255)).animate-pulse), i.e. the page relies heavily on deferred sections.main's server HTML is ~68.6 KB and matches the regenerated client tree, so nothing structural is missing/extra in a stable way.Suspected root cause
In
packages/tanstack/src/hooks/DecoPageRenderer.tsx:On the server this is always
null(skeleton is emitted), while on the client the first render — the hydration render — can already return resolved content. Any divergence here is a guaranteed hydration mismatch.<Suspense fallback={skeleton}><Await promise={promise}>…) can have its data resolved before hydration starts on slow/cold loads, which is consistent with the cold-cache-only reproduction: when the JS bundles are already cached, hydration wins the race and no error is reported.Suggested fix
Keep the first client render byte-identical to the SSR output and only adopt resolved/cached content in a later commit:
getCachedDeferredSection()insideuseEffect(after mount) instead of in theuseStateinitializer, oruseSyncExternalStorewithgetServerSnapshot()returningnullso server and hydration snapshots always agree, orhydratedflag set after the first commit.Related
useDevice()/ ALS losing context → feat(migrate): provision analytics metadata in decocms Supabase #418)I could not determine the exact
@decocms/startversion of this deployment (no version endpoint exposed), so it would be useful to know whether this is residual behaviour from something already fixed.