Skip to content

fix(react-query): hydrate deferred queries before observers subscribe to avoid a redundant refetch - #11137

Open
ostapondo wants to merge 1 commit into
TanStack:mainfrom
ostapondo:fix/hydration-boundary-deferred-refetch
Open

fix(react-query): hydrate deferred queries before observers subscribe to avoid a redundant refetch#11137
ostapondo wants to merge 1 commit into
TanStack:mainfrom
ostapondo:fix/hydration-boundary-deferred-refetch

Conversation

@ostapondo

@ostapondo ostapondo commented Aug 3, 2026

Copy link
Copy Markdown

🎯 Changes

Fixes #9610.

HydrationBoundary defers hydration of queries that already exist in the cache to an effect, so that transitions don't update existing observers mid-render. But useQuery's useSyncExternalStore subscription is also a passive effect, and children's effects run before parents'. On a revisit, where the query is cached but was unmounted, the observer subscribes first, sees the old stale entry and starts a refetch of the exact data the dehydrated state already contains. The server computes it in the loader and the client immediately fetches it again.

This moves the deferred hydration, and the optionsRef sync it reads, to a layout effect. All layout effects run before any passive effect, so hydration lands in the cache before a remounting observer decides whether to fetch. Layout effects still only run when the tree commits, so the aborted transition behaviour is unchanged and its test stays green.

Behaviour change worth noting: already-mounted observers now get hydrated data at commit time rather than after paint. One timing assertion in an existing test was updated for that. The render phase guarantee is still covered by the aborted transition test.

I tried a narrower version that only hydrates queries with no subscribers, so mounted observers keep their old timing. It doesn't work here: React cleans up a deleted subtree's subscriptions in the passive phase, after this layout effect, so when a page unmounts and remounts in the same commit the outgoing observer is still on the query and the refetch comes back. There's a test for that case now.

Verified with tests that fail on main and pass with the fix, plus a jsdom repro of the React Router loader flow from the issue against the built package.

Not covered: a useSuspenseQuery over a query that is in pending state with no data starts its fetch during render, before any layout effect. That pre-existing path is unaffected by this change.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved hydration timing so deferred query data is available before effects run.
    • Prevented remounted queries from unnecessarily refetching data already present in dehydrated state.
    • Preserved correct behavior for stale and inactive queries during hydration.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a708d21a-baff-416f-8302-0783a50a5868

📥 Commits

Reviewing files that changed from the base of the PR and between fca2f4c and ab12b64.

📒 Files selected for processing (1)
  • packages/react-query/src/__tests__/HydrationBoundary.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/react-query/src/tests/HydrationBoundary.test.tsx

📝 Walkthrough

Walkthrough

HydrationBoundary now hydrates deferred queries in an isomorphic layout effect. Tests verify immediate hydrated data and prevent redundant refetches when queries remount.

Changes

HydrationBoundary hydration timing

Layer / File(s) Summary
Isomorphic deferred hydration
packages/react-query/src/HydrationBoundary.tsx
HydrationBoundary uses useLayoutEffect in the browser and useEffect on the server. Queued hydration runs after commit and before children’s passive subscriptions.
Hydration remount validation
packages/react-query/src/__tests__/HydrationBoundary.test.tsx, .changeset/hungry-planes-tease.md
Tests verify immediate hydrated data for existing and remounted queries without a client refetch. A patch changeset documents the fix.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • TanStack/query issue 11155 — Addresses the same hydration race in the separate Preact HydrationBoundary.

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Loader
  participant HydrationBoundary
  participant QueryCache
  participant QueryComponent
  Loader->>HydrationBoundary: Provide dehydrated query state
  HydrationBoundary->>QueryCache: Hydrate queued query in layout effect
  QueryComponent->>QueryCache: Subscribe during remount
  QueryCache-->>QueryComponent: Return hydrated data
  QueryComponent->>QueryCache: Do not invoke client query function
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the HydrationBoundary fix and the prevented redundant refetch.
Description check ✅ Passed The description covers the change, motivation, testing, checklist, release impact, and known limitation.
Linked Issues check ✅ Passed The implementation and regression tests address issue #9610 by hydrating before remounted observers can trigger a duplicate refetch.
Out of Scope Changes check ✅ Passed The code, tests, and changeset directly support the linked issue and stated hydration behavior objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ostapondo
ostapondo force-pushed the fix/hydration-boundary-deferred-refetch branch 2 times, most recently from 6829c4d to fe46639 Compare August 9, 2026 17:00
@ostapondo ostapondo changed the title fix(react-query): hydrate deferred queries before observers subscribe to avoid a redundant refetch fix(react-query): hydrate unobserved queries before observers subscribe to avoid a redundant refetch Aug 9, 2026
@ostapondo

Copy link
Copy Markdown
Author

Updated after review feedback on #11157.

Before, this moved the whole queue to a layout effect, existing queries included, which also changed when already-mounted observers see hydrated data (commit time instead of after paint) and needed one assertion in an existing test to be relaxed. Only the unobserved half needs to move to fix the refetch, so that's all it does now. The test file is additions only, nothing existing changed, and "Existing observer should not have updated at this point" keeps guarding the observed case.

Same shape in #11157 now.

@ostapondo
ostapondo force-pushed the fix/hydration-boundary-deferred-refetch branch from fe46639 to fca2f4c Compare August 9, 2026 17:41
@ostapondo ostapondo changed the title fix(react-query): hydrate unobserved queries before observers subscribe to avoid a redundant refetch fix(react-query): hydrate deferred queries before observers subscribe to avoid a redundant refetch Aug 9, 2026
@ostapondo

Copy link
Copy Markdown
Author

Rolled the split back out of this one, it doesn't hold up in React.

React cleans up a removed subtree's subscriptions in the passive phase, after this layout effect runs. So when a page unmounts and remounts in the same commit, which is what happens as soon as the boundary wraps the page, the outgoing observer is still counted on the query, the split skips it and the refetch is back. Added a test for that shape, it fails with the split and passes with this.

So this is back to hydrating the whole queue in the layout effect, which is fine here because React never commits a render it throws away.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/react-query/src/__tests__/HydrationBoundary.test.tsx`:
- Around line 152-160: Update the HydrationBoundary test’s existing-observer
assertions so the prior value remains visible during render, and only the
hydrated value appears after passive effects flush. Keep the immediate “added”
assertion for newly introduced query data, then advance timers and assert the
existing query changes to “should change” afterward.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3bd29617-79dc-47a3-a8a6-86fe60bd2a2c

📥 Commits

Reviewing files that changed from the base of the PR and between fe46639 and fca2f4c.

📒 Files selected for processing (3)
  • .changeset/hungry-planes-tease.md
  • packages/react-query/src/HydrationBoundary.tsx
  • packages/react-query/src/__tests__/HydrationBoundary.test.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • .changeset/hungry-planes-tease.md
  • packages/react-query/src/HydrationBoundary.tsx

Comment thread packages/react-query/src/__tests__/HydrationBoundary.test.tsx Outdated
@ostapondo
ostapondo force-pushed the fix/hydration-boundary-deferred-refetch branch from fca2f4c to ab12b64 Compare August 9, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HydrationBoundary double fetching on subsequent visits

1 participant