Skip to content

Fix/infinite query hydration 8825#10074

Open
chosule wants to merge 4 commits intoTanStack:mainfrom
chosule:fix/infinite-query-hydration-8825
Open

Fix/infinite query hydration 8825#10074
chosule wants to merge 4 commits intoTanStack:mainfrom
chosule:fix/infinite-query-hydration-8825

Conversation

@chosule
Copy link
Copy Markdown

@chosule chosule commented Jan 27, 2026

Fixes #8825

Note: This solution was discussed and approved by @TkDodo in Discussion #10027

Problem

When prefetchInfiniteQuery fails on the server, useSuspenseInfiniteQuery on the client doesn't return the correct infinite query structure ({ pages: [], pageParams: [] }), causing a runtime error when accessing data.pages:

TypeError: Cannot read properties of undefined (reading 'length')

Root Cause

Infinite query behavior information gets lost during hydration:

  1. infiniteQueryBehavior is attached when prefetchInfiniteQuery runs on the server
  2. This behavior info isn't preserved during dehydration
  3. On the client, it gets restored as a regular query during hydration
  4. Error occurs when data is accessed before InfiniteQueryObserver attaches the behavior

Solution

Set up infinite query behavior at hydration time to guarantee correct data structure for both success and failure cases.

Changes

1. Added queryType field to DehydratedQuery

  • Metadata to identify infinite queries
  • Stores 'query' | 'infiniteQuery' value

2. Auto-detect query type during dehydration

  • Checks for initialPageParam presence to identify infinite queries
  • Stores result in queryType field

3. Auto-configure behavior during hydration

  • Attaches infiniteQueryBehavior when queryType is 'infiniteQuery'

4. Handle failed promises

  • Rejected promises aren't passed as initialPromise
  • Allows automatic retry on the client

Modified Files

  1. packages/query-core/src/hydration.ts
  2. packages/query-core/src/__tests__/hydration.test.tsx

Tests

All 35 tests passing (including 2 new tests)

New tests added:

  • should preserve queryType for infinite queries during hydration
  • should attach infiniteQueryBehavior during hydration

Checklist

  • Added queryType: 'query' | 'infiniteQuery' field to DehydratedQuery
  • Auto-detect infinite queries during dehydration by checking initialPageParam
  • Auto-configure infiniteQueryBehavior for infinite queries during hydration
  • Added test cases for hydrated infinite query behavior
  • Guarantee correct data structure for both success and failure cases

Summary by CodeRabbit

  • Bug Fixes

    • Preserve infinite-query pagination and behavior during SSR hydration so hydrated clients retain pages, cursors, and correct subsequent fetch behavior.
  • Tests

    • Added hydration tests to validate infinite query preservation, restoration, and post-hydration fetching.
  • Chores

    • Added release metadata documenting this patch release.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Jan 27, 2026

🦋 Changeset detected

Latest commit: 7c41b00

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

This PR includes changesets to release 19 packages
Name Type
@tanstack/query-core Patch
@tanstack/angular-query-experimental 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/react-query Patch
@tanstack/solid-query Patch
@tanstack/svelte-query Patch
@tanstack/vue-query Patch
@tanstack/angular-query-persist-client Patch
@tanstack/react-query-persist-client Patch
@tanstack/solid-query-persist-client Patch
@tanstack/svelte-query-persist-client Patch
@tanstack/react-query-devtools Patch
@tanstack/react-query-next-experimental 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 Jan 27, 2026

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
📝 Walkthrough

Walkthrough

Dehydration now tags queries with queryType (including infiniteQuery) and hydration restores that type, applying infiniteQueryBehavior for restored infinite queries. Two tests and a changeset were added to validate preservation of pages, pageParams, and behavior across dehydrate→hydrate.

Changes

Cohort / File(s) Summary
Metadata
\.changeset/stupid-seals-live.md
New changeset marking a patch for @tanstack/query-core describing fix to preserve infinite query behavior during SSR hydration.
Core Hydration Logic
packages/query-core/src/hydration.ts
DehydratedQuery gains `queryType?: 'query'
Hydration Tests
packages/query-core/src/__tests__/hydration.test.tsx
Added two tests: one asserting queryType and pages/pageParams are preserved after dehydrate→hydrate for infinite queries; another verifying infiniteQueryBehavior is attached so fetchInfiniteQuery returns expected pages and pageParams post-hydration.

Sequence Diagram(s)

sequenceDiagram
  participant Server as Server (prefetch)
  participant Dehydrator as Dehydrator
  participant Serialized as Dehydrated State
  participant Client as Client (hydrate)
  participant QueryClient as QueryClient

  Server->>Dehydrator: prefetchInfiniteQuery (produce pages, pageParams)
  Dehydrator->>Serialized: dehydrate → include query + queryType:'infiniteQuery'
  Serialized-->>Client: transfer dehydrated state
  Client->>QueryClient: hydrate(state)
  QueryClient->>QueryClient: restore queries, read queryType
  alt queryType == 'infiniteQuery'
    QueryClient->>QueryClient: set behavior = infiniteQueryBehavior()
  end
  Client->>QueryClient: fetchInfiniteQuery() → uses restored pages & pageParams
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

"🐰 I hopped through states both wide and deep,
Saved pages and cursors for you to keep,
Dehydrate, hydrate — structure held tight,
Infinite hops return just right,
A joyful rabbit's tiny leap!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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 'Fix/infinite query hydration 8825' clearly refers to the primary change: fixing infinite query hydration behavior related to issue #8825.
Description check ✅ Passed The description includes all required template sections: problem explanation, root cause analysis, solution overview, detailed changes, modified files, and test results with checklist completion.
Linked Issues check ✅ Passed All coding requirements from #8825 are addressed: the PR preserves infinite-query data structure during hydration, fixes the TypeError by auto-configuring behavior at hydration time, and includes tests for both success and failure scenarios.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing infinite query hydration: added queryType field to DehydratedQuery, auto-detection logic, behavior configuration during hydration, and corresponding test cases.

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

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

@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Jan 29, 2026

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit 161c143

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

☁️ Nx Cloud last updated this comment at 2026-01-30 09:25:05 UTC

Comment on lines +258 to +264
const queryOptions: any = {
...client.getDefaultOptions().hydrate?.queries,
...options?.defaultOptions?.queries,
queryKey,
queryHash,
meta,
}
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.

can we keep this inlined please and just conditionally assign behavior:

behavior: queryType === 'infiniteQuery'
  ? (infiniteQueryBehavior() as QueryBehavior<unknown, unknown, unknown>)
  : undefined,

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.

this is still not addressed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed any from queryOptions and changed the cast to QueryBehavior<unknown, DefaultError, unknown>.
Is this the direction you had in mind?

Comment on lines +297 to +312
const isRejectedThenable =
promise &&
typeof promise === 'object' &&
'status' in promise &&
(promise as any).status === 'rejected'

if (!isRejectedThenable) {
query
.fetch(undefined, {
// RSC transformed promises are not thenable
initialPromise: Promise.resolve(promise).then((resolvedData) => {
return deserializeData(resolvedData)
}),
})
// Avoid unhandled promise rejections
.catch(noop)
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 don’t understand what this change has to do with infinite queries ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@TkDodo
Applied your feedback! 🙏

You're right that the rejected promise handling wasn't directly related to #8825.
I've removed that part and inlined the behavior assignment as suggested.

Please take another look when you can!

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Jan 30, 2026

More templates

@tanstack/angular-query-experimental

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

@tanstack/eslint-plugin-query

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

@tanstack/query-async-storage-persister

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

@tanstack/query-broadcast-client-experimental

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

@tanstack/query-core

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

@tanstack/query-devtools

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

@tanstack/query-persist-client-core

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

@tanstack/query-sync-storage-persister

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

@tanstack/react-query

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

@tanstack/react-query-devtools

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

@tanstack/react-query-next-experimental

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

@tanstack/react-query-persist-client

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

@tanstack/solid-query

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

@tanstack/solid-query-devtools

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

@tanstack/solid-query-persist-client

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

@tanstack/svelte-query

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

@tanstack/svelte-query-devtools

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

@tanstack/svelte-query-persist-client

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

@tanstack/vue-query

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

@tanstack/vue-query-devtools

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

commit: 161c143

@chosule chosule force-pushed the fix/infinite-query-hydration-8825 branch from 161c143 to 7c41b00 Compare January 30, 2026 13:25
@chosule
Copy link
Copy Markdown
Author

chosule commented Mar 19, 2026

Hi @TkDodo, gentle ping! Let me know if there's anything else to address.

}

function isInfiniteQuery(query: Query): boolean {
const options = query.options as any
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.

the as any seems super unnecessary

Suggested change
const options = query.options as any
const options = query.options

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed as any from query.options.

Comment on lines +258 to +264
const queryOptions: any = {
...client.getDefaultOptions().hydrate?.queries,
...options?.defaultOptions?.queries,
queryKey,
queryHash,
meta,
}
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.

this is still not addressed

@chosule chosule force-pushed the fix/infinite-query-hydration-8825 branch from 7c41b00 to 9e1ee4d Compare March 24, 2026 12:42
@chosule chosule force-pushed the fix/infinite-query-hydration-8825 branch from 9e1ee4d to 47b6409 Compare March 24, 2026 12:44
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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/query-core/src/hydration.ts`:
- Around line 75-78: The isInfiniteQuery check loses the infinite-query marker
because rebuilt queries (around the rebuild logic at lines 258-267) only copy
behavior and drop options like initialPageParam; update the rebuild logic so
when reconstructing a Query you preserve options.initialPageParam (or any
presence of that key) from the original query.options into the new options
object so isInfiniteQuery(query) still returns true; specifically, in the code
that recreates the query instance, copy over initialPageParam if it exists (or
spread the original options) so the isInfiniteQuery function and subsequent
dehydrate() keep the entry as an infinite query.
- Around line 14-16: The import ordering currently violates the lint rule
because the value import infiniteQueryBehavior is placed after type-only
imports; move the value import "infiniteQueryBehavior" so it appears before the
type-only imports "Query, QueryBehavior, QueryState" and "Mutation,
MutationState" (i.e., import infiniteQueryBehavior first, then the type-only
imports) to satisfy the import/order rule while keeping the same symbols and
module names.
- Around line 258-267: The merge for queryOptions is overwriting any
caller-provided hydrate.behavior with explicit undefined because behavior is
always assigned; change it to only set behavior when queryType ===
'infiniteQuery' so existing behavior from
client.getDefaultOptions().hydrate?.queries or options?.defaultOptions?.queries
is preserved. Concretely, build the base merge of
client.getDefaultOptions().hydrate?.queries and options?.defaultOptions?.queries
into queryOptions (including queryKey/queryHash/meta), then conditionally add
behavior using infiniteQueryBehavior() only when queryType === 'infiniteQuery'
(or spread { behavior: ... } in that branch) instead of assigning behavior:
undefined in the default path; update the code around the queryOptions object
(symbols: queryOptions, client.getDefaultOptions().hydrate?.queries,
options?.defaultOptions?.queries, behavior, infiniteQueryBehavior, queryType)
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8dab147c-bba2-400f-8e55-09237251b4e8

📥 Commits

Reviewing files that changed from the base of the PR and between 9e1ee4d and 47b6409.

📒 Files selected for processing (1)
  • packages/query-core/src/hydration.ts

Comment on lines +14 to +16
import type { Query, QueryBehavior, QueryState } from './query'
import type { Mutation, MutationState } from './mutation'
import { infiniteQueryBehavior } from './infiniteQueryBehavior'
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.

⚠️ Potential issue | 🟡 Minor

Fix the import/order failure.

Move import { infiniteQueryBehavior } from './infiniteQueryBehavior' above the type-only imports. Line 16 currently trips the ESLint rule.

🧰 Tools
🪛 ESLint

[error] 16-16: ./infiniteQueryBehavior import should occur before type import of ./types

(import/order)

🤖 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 14 - 16, The import
ordering currently violates the lint rule because the value import
infiniteQueryBehavior is placed after type-only imports; move the value import
"infiniteQueryBehavior" so it appears before the type-only imports "Query,
QueryBehavior, QueryState" and "Mutation, MutationState" (i.e., import
infiniteQueryBehavior first, then the type-only imports) to satisfy the
import/order rule while keeping the same symbols and module names.

Comment on lines +75 to +78
function isInfiniteQuery(query: Query): boolean {
const options = query.options
return 'initialPageParam' in options
}
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.

⚠️ Potential issue | 🟠 Major

isInfiniteQuery becomes lossy after this hydration path.

Lines 258-267 rebuild infinite queries with behavior only, so the original initialPageParam marker is lost here. Unless hydrate defaults happen to re-add it, Line 77 flips back to false on the rebuilt query, and a later dehydrate() serializes the same entry as a regular query again. That reopens a hydrate→dehydrate round-trip gap for the pending infinite-query path this change is fixing.

Also applies to: 258-267

🤖 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 75 - 78, The
isInfiniteQuery check loses the infinite-query marker because rebuilt queries
(around the rebuild logic at lines 258-267) only copy behavior and drop options
like initialPageParam; update the rebuild logic so when reconstructing a Query
you preserve options.initialPageParam (or any presence of that key) from the
original query.options into the new options object so isInfiniteQuery(query)
still returns true; specifically, in the code that recreates the query instance,
copy over initialPageParam if it exists (or spread the original options) so the
isInfiniteQuery function and subsequent dehydrate() keep the entry as an
infinite query.

Comment on lines +258 to +267
const queryOptions = {
...client.getDefaultOptions().hydrate?.queries,
...options?.defaultOptions?.queries,
queryKey,
queryHash,
meta,
behavior: queryType === 'infiniteQuery'
? (infiniteQueryBehavior() as QueryBehavior<unknown, DefaultError, unknown>)
: undefined,
}
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.

⚠️ Potential issue | 🟠 Major

Don't overwrite caller-provided hydrate behavior with undefined.

Because Lines 264-266 are assigned last, non-infinite or legacy payloads now overwrite any hydrate.defaultOptions.queries.behavior with undefined. The previous merge preserved that option, so this is a behavior regression for custom hydration setups.

Suggested fix
         const queryOptions = {
           ...client.getDefaultOptions().hydrate?.queries,
           ...options?.defaultOptions?.queries,
           queryKey,
           queryHash,
           meta,
-          behavior: queryType === 'infiniteQuery'
-          ? (infiniteQueryBehavior() as QueryBehavior<unknown, DefaultError, unknown>)
-          : undefined,
+          ...(queryType === 'infiniteQuery'
+            ? {
+                behavior: infiniteQueryBehavior() as QueryBehavior<
+                  unknown,
+                  DefaultError,
+                  unknown
+                >,
+              }
+            : {}),
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const queryOptions = {
...client.getDefaultOptions().hydrate?.queries,
...options?.defaultOptions?.queries,
queryKey,
queryHash,
meta,
behavior: queryType === 'infiniteQuery'
? (infiniteQueryBehavior() as QueryBehavior<unknown, DefaultError, unknown>)
: undefined,
}
const queryOptions = {
...client.getDefaultOptions().hydrate?.queries,
...options?.defaultOptions?.queries,
queryKey,
queryHash,
meta,
...(queryType === 'infiniteQuery'
? {
behavior: infiniteQueryBehavior() as QueryBehavior<
unknown,
DefaultError,
unknown
>,
}
: {}),
}
🤖 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 258 - 267, The merge for
queryOptions is overwriting any caller-provided hydrate.behavior with explicit
undefined because behavior is always assigned; change it to only set behavior
when queryType === 'infiniteQuery' so existing behavior from
client.getDefaultOptions().hydrate?.queries or options?.defaultOptions?.queries
is preserved. Concretely, build the base merge of
client.getDefaultOptions().hydrate?.queries and options?.defaultOptions?.queries
into queryOptions (including queryKey/queryHash/meta), then conditionally add
behavior using infiniteQueryBehavior() only when queryType === 'infiniteQuery'
(or spread { behavior: ... } in that branch) instead of assigning behavior:
undefined in the default path; update the code around the queryOptions object
(symbols: queryOptions, client.getDefaultOptions().hydrate?.queries,
options?.defaultOptions?.queries, behavior, infiniteQueryBehavior, queryType)
accordingly.

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.

Next.js: exception encountered in prefetchInfiniteQuery causes useSuspenseInfiniteQuery to fail

2 participants