Skip to content

Prevent useQuery from skipping hydration#10159

Open
ColemanDunn wants to merge 6 commits intoTanStack:mainfrom
ColemanDunn:prevent-usequery-from-blocking-hydration
Open

Prevent useQuery from skipping hydration#10159
ColemanDunn wants to merge 6 commits intoTanStack:mainfrom
ColemanDunn:prevent-usequery-from-blocking-hydration

Conversation

@ColemanDunn
Copy link
Copy Markdown
Contributor

@ColemanDunn ColemanDunn commented Feb 21, 2026

🎯 Changes

Fixes #10145

Hydration is being skipped for useQueries that add to the cache above a HydrationBoundary but will actually not run or hydrate the queryClient.

✅ 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).

Summary by CodeRabbit

  • Bug Fixes

    • Hydration now includes idle/pending queries to avoid unnecessary refetches or suspense-triggered reloads.
  • New Features

    • Added two public APIs: defaultShouldDehydrateQuery and useSuspenseQuery.
  • Tests

    • Added coverage for idle-query hydration behavior inside Suspense boundaries.
  • Chores

    • Published patch updates for two TanStack libraries.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 21, 2026

🦋 Changeset detected

Latest commit: 36edbba

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@tanstack/react-query Patch
@tanstack/query-core Patch
@tanstack/react-query-devtools Patch
@tanstack/react-query-next-experimental Patch
@tanstack/react-query-persist-client Patch
@tanstack/angular-query-experimental Patch
@tanstack/preact-query Patch
@tanstack/query-async-storage-persister Patch
@tanstack/query-broadcast-client-experimental Patch
@tanstack/query-persist-client-core Patch
@tanstack/query-sync-storage-persister Patch
@tanstack/solid-query Patch
@tanstack/svelte-query Patch
@tanstack/vue-query Patch
@tanstack/angular-query-persist-client Patch
@tanstack/solid-query-persist-client Patch
@tanstack/svelte-query-persist-client Patch
@tanstack/solid-query-devtools Patch
@tanstack/svelte-query-devtools Patch
@tanstack/vue-query-devtools Patch

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 21, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Hydration logic was changed so existing idle/pending queries (dataUpdatedAt === 0, status === "pending", fetchStatus === "idle") are treated like absent queries during hydration; a changeset bumps react-query and query-core as patch releases and tests added to verify no suspense-triggered refetch occurs.

Changes

Cohort / File(s) Summary
Changeset Entry
.changeset/moody-cities-stand.md
Adds a changeset bumping @tanstack/react-query and @tanstack/query-core (patch) with note to prevent registered useQueries from skipping hydration.
Query-core hydration logic
packages/query-core/src/hydration.ts
Introduces existingQueryIsUndefinedOrIsIdleUseQuery condition and updates hydration checks so existing idle/pending queries are eligible for hydration (adjusts promise hydration trigger and preservation of dehydration ordering).
React Hydration boundary
packages/react-query/src/HydrationBoundary.tsx
Adds condition to treat existing queries with dataUpdatedAt===0, status==='pending', fetchStatus==='idle' as candidates for hydration, pushing them into the hydration flow.
Tests / Public exports
packages/react-query/src/__tests__/HydrationBoundary.test.tsx
Adds tests that validate hydrating pending-idle queries in a Suspense render to avoid refetch; re-exports used helpers/hooks for test scenarios.

Sequence Diagram(s)

sequenceDiagram
    participant Server
    participant HydrationBoundary
    participant QueryClient
    participant ClientComponent

    Server->>QueryClient: prefetchQuery(key, data)
    Server->>HydrationBoundary: provide dehydrated state
    ClientComponent->>HydrationBoundary: render useQuery / useSuspenseQuery
    HydrationBoundary->>QueryClient: check existingQuery state
    alt existingQuery missing OR idle/pending (dataUpdatedAt==0, pending, idle)
        HydrationBoundary->>QueryClient: hydrate dehydratedQuery -> replace/cache data
    else existingQuery active/fetching
        HydrationBoundary->>QueryClient: skip hydration for this key
    end
    ClientComponent->>QueryClient: read cached data (no suspense refetch)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • TkDodo
  • manudeli

Poem

🐇 I hopped where dehydrated values lay,
Found sleepy queries not quite awake today.
I nudged them gently into cached embrace,
No duplicate fetches, no frantic race.
Carrot cheers — hydration saved the place.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Prevent useQuery from skipping hydration' directly addresses the primary change, clearly summarizing the fix for the hydration skipping bug.
Description check ✅ Passed The description follows the template with all required sections completed: 🎯 Changes section references the linked issue #10145 and describes the bug, checklist items are marked complete, and 🚀 Release Impact indicates a changeset was generated.
Linked Issues check ✅ Passed The code changes successfully address issue #10145 by modifying hydration logic to treat pending idle queries as candidates for hydration, preventing skipping of server-prefetched queries when parent useQuery renders earlier.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the hydration issue: hydration.ts updates core logic, HydrationBoundary.tsx applies the fix in React implementation, and test additions validate the scenario from issue #10145.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

@ColemanDunn ColemanDunn force-pushed the prevent-usequery-from-blocking-hydration branch from 5fedd07 to 45eb471 Compare February 21, 2026 08:34
@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Feb 21, 2026

View your CI Pipeline Execution ↗ for commit 03b08ab

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 5m 32s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2m 19s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-25 23:41:09 UTC

@ColemanDunn ColemanDunn force-pushed the prevent-usequery-from-blocking-hydration branch from 24dbb40 to 133a6ca Compare February 21, 2026 08:37
@ColemanDunn ColemanDunn force-pushed the prevent-usequery-from-blocking-hydration branch from 133a6ca to de84710 Compare February 21, 2026 08:40
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Feb 21, 2026

More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@10159

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@10159

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@10159

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/@tanstack/preact-query-devtools@10159

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/@tanstack/preact-query-persist-client@10159

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@10159

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@10159

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@10159

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@10159

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@10159

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@10159

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@10159

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@10159

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@10159

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@10159

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@10159

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@10159

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@10159

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@10159

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@10159

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@10159

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@10159

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@10159

commit: ac27167

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/react-query/src/HydrationBoundary.tsx (1)

76-79: Consider merging the two newQueries.push branches.

Both branches push to the same array. Combining them simplifies the control flow.

♻️ Optional simplification
-          if (!existingQuery) {
-            newQueries.push(dehydratedQuery)
-          } else if (existingQueryIsIdleUseQuery) {
+          if (!existingQuery || existingQueryIsIdleUseQuery) {
             newQueries.push(dehydratedQuery)
           } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-query/src/HydrationBoundary.tsx` around lines 76 - 79, The two
branches that both call newQueries.push(dehydratedQuery) can be merged: replace
the separate if (!existingQuery) { newQueries.push(dehydratedQuery) } else if
(existingQueryIsIdleUseQuery) { newQueries.push(dehydratedQuery) } with a single
conditional that pushes when either condition is true (e.g., if (!existingQuery
|| existingQueryIsIdleUseQuery) newQueries.push(dehydratedQuery)); update the
logic around existingQuery and existingQueryIsIdleUseQuery so no behavior
changes occur beyond the simplification.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/moody-cities-stand.md:
- Line 6: Fix the typo "hyrdation" to "hydration" in the generated changelog
text by updating the string "prevent registered useQueries from skipping
hyrdation" to "prevent registered useQueries from skipping hydration" (search
for the exact misspelled token "hyrdation" in the changelog entry and replace
it).

---

Nitpick comments:
In `@packages/react-query/src/HydrationBoundary.tsx`:
- Around line 76-79: The two branches that both call
newQueries.push(dehydratedQuery) can be merged: replace the separate if
(!existingQuery) { newQueries.push(dehydratedQuery) } else if
(existingQueryIsIdleUseQuery) { newQueries.push(dehydratedQuery) } with a single
conditional that pushes when either condition is true (e.g., if (!existingQuery
|| existingQueryIsIdleUseQuery) newQueries.push(dehydratedQuery)); update the
logic around existingQuery and existingQueryIsIdleUseQuery so no behavior
changes occur beyond the simplification.

Comment thread .changeset/moody-cities-stand.md Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/query-core/src/hydration.ts (1)

220-224: existingQueryIsUndefinedOrIsIdleUseQuery — naming couples implementation to a specific caller

The three-condition guard (dataUpdatedAt === 0 && status === 'pending' && fetchStatus === 'idle') characterises a structural query state (never-fetched and idle), not the type of observer that created the entry. The UseQuery suffix implies caller-origin knowledge that isn't actually verified here. A name like existingQueryIsUndefinedOrNeverFetchedIdle or queryIsAbsentOrIdlePending would be less misleading to future readers.

♻️ Suggested rename
-      const existingQueryIsUndefinedOrIsIdleUseQuery =
+      const existingQueryIsAbsentOrNeverFetchedIdle =
         !query ||
         (query.state.dataUpdatedAt === 0 &&
           query.state.status === 'pending' &&
           query.state.fetchStatus === 'idle')

And update the usage at line 270:

-        (existingQueryIsUndefinedOrIsIdleUseQuery ||
+        (existingQueryIsAbsentOrNeverFetchedIdle ||
           (!existingQueryIsPending && !existingQueryIsFetching)) &&
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/query-core/src/hydration.ts` around lines 220 - 224, Rename the
misleading variable existingQueryIsUndefinedOrIsIdleUseQuery to a name that
reflects the structural state check (for example
existingQueryIsUndefinedOrNeverFetchedIdle or queryIsAbsentOrIdlePending) and
update all references (including the use at the site that currently reads the
old name) so the code checks the same three-condition guard (query is falsy OR
query.state.dataUpdatedAt === 0 && query.state.status === 'pending' &&
query.state.fetchStatus === 'idle') but without implying a specific caller like
UseQuery; update both the variable declaration and every place it is referenced
(e.g., the expression currently using existingQueryIsUndefinedOrIsIdleUseQuery).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/query-core/src/hydration.ts`:
- Around line 220-224: Rename the misleading variable
existingQueryIsUndefinedOrIsIdleUseQuery to a name that reflects the structural
state check (for example existingQueryIsUndefinedOrNeverFetchedIdle or
queryIsAbsentOrIdlePending) and update all references (including the use at the
site that currently reads the old name) so the code checks the same
three-condition guard (query is falsy OR query.state.dataUpdatedAt === 0 &&
query.state.status === 'pending' && query.state.fetchStatus === 'idle') but
without implying a specific caller like UseQuery; update both the variable
declaration and every place it is referenced (e.g., the expression currently
using existingQueryIsUndefinedOrIsIdleUseQuery).

@TkDodo
Copy link
Copy Markdown
Collaborator

TkDodo commented Apr 25, 2026

@Ephem can you take a look here please

@ColemanDunn please fix the conflicts

@TkDodo TkDodo requested a review from Ephem April 25, 2026 15:59
…om-blocking-hydration

# Conflicts:
#	packages/query-core/src/hydration.ts
#	packages/react-query/src/__tests__/HydrationBoundary.test.tsx
@ColemanDunn
Copy link
Copy Markdown
Contributor Author

@ColemanDunn please fix the conflicts

Done! Thank you!

@ColemanDunn
Copy link
Copy Markdown
Contributor Author

ColemanDunn commented Apr 25, 2026

There were some new considerations and tests I added in for the new syncData changes in the merge conflict: ac27167

Copy link
Copy Markdown
Collaborator

@Ephem Ephem left a comment

Choose a reason for hiding this comment

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

Thanks for trying to tackle this! I have to think some more about this, but I think this fix might be a bit too simplistic.

query?.state.dataUpdatedAt === 0 &&
query.state.status === 'pending' &&
query.state.fetchStatus === 'idle'

This part is a heuristic for trying to determine when it is safe to hydrate, even if the query is already in the cache. Unfortunately, there is no such time. First, there are more situations than the reproduction itself that can cause this set of states, and it's hard to foresee all the possible side effects.

Second, imagine this case:

<UseQueryComponent>
  <HydrationBoundary>
    <Suspense>
      <UseSuspenseQueryComponent>

Anything in the Suspense can hydrate later than the <UseQueryComponent>. By that time, the observer in the <UseQueryComponent> will have been set up, so if we hydrate into the cache, we are now triggering an observed side effect in render, which is not safe.

The real problem here is that different subtrees should be able to see different data for a single query during hydration. This is not possible with the current approach, which is why this bug has gone unfixed for so long.

The planned fix for this is to avoid hydrating in render entirely. Instead we put the data to be hydrated on a context, meaning different subtrees can see different data during hydration, and a single subtree can always see the data that it was rendered with on the server. We then make all hooks read from that hydration context when necessary. We then hydrate in a useEffect at the end and remove the context data after hydration.

This separates UI reads during hydration from the cache, which is also a prerequisite for better concurrent rendering support, and it also fixes some other edge cases we have.

All that said, if we can find a safe quick fix for the issue at hand, I'm all for it, but I'm not certain yet this is one, and I'm not sure what it would take to feel safe since this is so complex to reason about. 😄


Note that a useQuery before the point of hydration should always be expected to sometimes start its own fetch on the client, even after a fix, imagine:

<UseQueryComponent>
  <Suspense>
    <UnrelatedComponentThatSuspends>
      <HydrationBoundary>
        <UseSuspenseQueryComponent>

In this case, which might look contrived but is not uncommon in complex apps, the unrelated component makes it so the <UseQueryComponent> has time to start a fetch on the client before we even hit hydration. Still something we need to fix ofc, just thought I'd mention another footgun of the pattern. 😄

Comment thread packages/query-core/src/hydration.ts Outdated
Comment on lines +298 to +301
(existingQueryIsIdleUseQuery ||
// If the data was synchronously available, there is no need to set up
// a retryer and thus no reason to call fetch
(!syncData &&
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this logic is wrong. If existingQueryIsIdleUseQuery is true, we'll always call fetch, even if data was synchronously available. This causes a regression where the query quickly flashes the loading state when it shouldn't.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ThanksI I overcorrected on that follow up commit after the merge. I have updated the code and added a test to check the loading state is in the correct state.

I know you mentioned it is hard to foresee the side effects, but I hope there are some tests we can add to determine if we can find a safe enough fix for this.

@ColemanDunn
Copy link
Copy Markdown
Contributor Author

Thanks for the reply @Ephem! Very well put and makes sense.

The pattern with this shape:

<UseQueryComponent>
  <HydrationBoundary>
    <Suspense>
      <UseSuspenseQueryComponent>

has been something that is pretty common in projects I have worked on (think breadcrumbs that use useQuery and depend on something like useParams in a parent layout) so I appreciate being able to find a safe fix for this. I'll look into more options and write tests to ensure safety as much as we can for this change.

Are there any specific tests that should be added to this PR that that can can ensure the fix is safe? Perhaps one for this case:

<UseQueryComponent>
  <Suspense>
    <UnrelatedComponentThatSuspends>
      <HydrationBoundary>
        <UseSuspenseQueryComponent>

Thanks!

@Ephem
Copy link
Copy Markdown
Collaborator

Ephem commented Apr 26, 2026

@ColemanDunn Thanks for keeping a constructive attitude in the face of my pessimism! 😅


An aside on a possible workaround:

I've run into this case myself, where I feel I need a useQuery in a place above where I can prefetch said query. I ended up solving it by setting up a manual cache subscription to the store, along these lines:

  return useSyncExternalStore(
    useCallback(
      (onStoreChange: () => void) =>
        cache.subscribe(event => {
          if (
            event.query.queryKey.length === queryKey.length &&
            event.query.queryKey.every(
              // The === check here will only work for primitives, to support complex keys one would need to deep compare
              (k: unknown, i: number) => k === queryKey[i],
            )
          ) {
            onStoreChange();
          }
        }),
      [cache, queryKey],
    ),
    () => queryClient.getQueryData(queryKey),
    () => queryClient.getQueryData(queryKey),
  );

Maybe that can be a workaround for you to until this is fixed?


Back to the PR. Thinking about this some more, the thing I worry most about is probably the "hydrate in render after a store subscription has already been set up" case. You could likely get around that by also checking explicitly for whether the query has an active listener by using query.isActive(), and bailing out from hydrating in render if it does. 🤔

I haven't had time to think very deeply about this, no promises it makes the PR safe enough to merge as a quick fix etc, but I think it definitely helps!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HydrationBoundary ignores/skips server-prefetched query when a parent useQuery with same key is rendered before child useSuspenseQuery

3 participants