Repository navigation
fix(react-start-client): call HydrationBoundary hooks before the visible() gate - #8653
Conversation
🦋 Changeset detectedLatest commit: 47b3841 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd test it through hydration - Destructure only `o` and read `props.g` inline: keeps the deferred-hydration client gzip at main's (+6 B with both destructured after the reorder). - Replace the client-render test with a hydration test that runs the intersection in each task around the boundary's first hydration attempt and asserts no recoverable error. It fails on main. - Use the changeset wording of TanStack#8817. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
View your CI Pipeline Execution ↗ for commit 47b3841
☁️ Nx Cloud last updated this comment at |
Merging this PR will improve performance by 0.84%2 warnings, 1 note
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
#8817's side for the split-out PRs: - #8861 (Start router options): the PR's generator-config, Vite and Rsbuild wiring; main's start-router-wiring test and start-app helper dropped (start-build-options, start-dev-route-tree-footer and start-compose-project-path test the same four behaviours). Main's changeset kept; the duplicated bullets leave start-compiler-yuku.md. - #8860 (split route source maps): the PR's own loader; compilers.ts stays deleted; main's source-maps test dropped (build-source-maps and dev-source-maps test webpack, Rspack and Vite dev). unplugin ^3.5.0 kept from main (no downgrade). Main's changeset kept; the PR's source map clause leaves router-plugin-yuku.md. - #8653 (visible() hooks): merged to the PR's file, test and changeset unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🎯 Changes
Fixes #8652. The fix and its root-cause analysis come from this PR's original commit (9b225fd). A maintainer took the PR over to align it with the compiler rewrite (#8817), which carries the same fix.
The bug
<Hydrate when={visible()}>rendersHydrationBoundary, which suspends on its gate withif (!g.r) use(g.p)and calleduseEffectonly after that check. React replays a component that suspended inusewith its re-render dispatcher, and onlyuseswitches away from it. When the intersection arrives while React waits to replay,g.ris already true, so the replay skipsuseand reaches a hook the first attempt never called. React throws "Update hook called on initial render" (#467 in production). During hydration React then drops the boundary's server HTML and client-renders it: the fallback replaces the markup until the split code loads, andonRecoverableErrorreports the error.The fix
packages/react-start-client/src/hydration/visible.tsx: the effect moves above the gate, so every attempt calls the same hooks. Effects run only after commit, soonHydratedstill fires once, after the boundary renders its children.The component destructures only
oand readsprops.ginline. Keepingconst { g, o } = propsafter the reorder costs +6 B gzip onreact-start.deferred-hydration. The file is now identical to #8817's.The test
src/tests/Hydrate.test.tsx, "hydrates a visible boundary whichever task the intersection arrives in": it server-renders avisible()boundary, hydrates that HTML withhydrateRoot, and fires the intersection in each of the 12 tasks around the first hydration attempt. It asserts that every run hydrates (onHydratedresolves) with no recoverable error. It fails onmain(the error appears when the intersection lands one task in) and passes with the fix.This test replaces the earlier
src/tests/visible.test.tsx, which also failed onmain. That test triggered the replay from a client-onlycreateRootrender through a side effect during render. The hydration test covers the case users hit (server HTML being dropped), doesn't depend on one scheduling trick, and is the same test #8817 has. Keeping both would test the same thing twice.The changeset is renamed to
.changeset/visible-hydrate-replay.md, with #8817's wording (patch).Validation
@tanstack/react-start-client:test:unit,test:types,test:eslint,test:buildpass.tanstack-react-start-e2e-deferred-hydration(16 tests, dev and prod) andtanstack-react-start-e2e-rsc-deferred-hydration(3 tests, dev and prod) pass.Bundle size (client, against
mainatad5b3b7a09)react-start.deferred-hydrationThe original shape (
const { g, o } = propsafter the reorder) measured +6 B gzip, +0 raw and −8 B brotli onreact-start.deferred-hydration. These are the same numbers #8817 reports for itsvisible()fix.Relation to #8817
The three files here are byte-identical to #8817's (
visible.tsx, theHydrate.test.tsxtest, the changeset). A test merge of this branch intomainfollowed by #8817's head adds no conflicts: #8817's conflict set withmainstays the same 19 unrelated files. After this lands, #8817 only needs to drop thevisible()bundle-size note from its description.✅ Checklist
🚀 Release Impact
🤖 Generated with Claude Code
Summary by CodeRabbit