decocms / decocms/blocks

React #418 on cold-cache loads: deferred sections can resolve before hydration (DecoPageRenderer seeds first client render from the deferred cache)

Open
#443 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
5
Forks
2
Avg merge
20h 12m
Merged PRs (30d)
36

Description

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

  • #205 (Suspense wrapper for sync sections → #418, released in 6.7.3)
  • #231 / #278 (useDevice() / ALS losing context → #418)

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.

Contributor guide

No contributing guide indexed for this repository

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

Read packages/tanstack/src/hooks/DecoPageRenderer.tsx around lines 258-259 and 539-557, then reproduce the issue with a cold browser cache and early console.error/error capture. Trace whether deferred content is available during the hydration render and compare it with the SSR skeleton output. Done means cold-cache hydration no longer reports React error #418 or replaces the server-rendered tree.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.