Skip to content

fix(query-core,react-query): preserve infinite query type through failed SSR hydration#9633

Closed
joseph0926 wants to merge 1 commit intoTanStack:mainfrom
joseph0926:fix/prfetch-infinite-hydration
Closed

fix(query-core,react-query): preserve infinite query type through failed SSR hydration#9633
joseph0926 wants to merge 1 commit intoTanStack:mainfrom
joseph0926:fix/prfetch-infinite-hydration

Conversation

@joseph0926
Copy link
Copy Markdown
Contributor

@joseph0926 joseph0926 commented Sep 9, 2025

Fixes #8825

Problem

When prefetchInfiniteQuery fails on the server and useSuspenseInfiniteQuery later retries on the client, the response is treated as a regular query rather than the expected infinite structure ({ pages: [], pageParams: [] }). As a result, accessors like data.pages can throw runtime errors (for example, “Cannot read properties of undefined (reading 'length')”).

Maybe Root cause

During SSR, a failed infinite query loses its infinite-query type information at dehydration time. The hydrated instance therefore lacks the behavior that marks it as infinite. When a retry occurs on the client, the retryer can remain pending, and even after InfiniteQueryObserver attaches the proper behavior, the data is not transformed into the infinite shape. Together, these conditions cause the client recovery path to return a regular query payload instead of { pages, pageParams }.

Solution

The fix preserves infinite-query type information across the hydration boundary and ensures the retryer can progress for hydrated infinite queries. Concretely, a new isInfiniteQuery flag is added to the dehydrate/hydrate flow so the query is recognized as infinite after hydration. In addition, when a hydrated infinite query is found with a pending retryer, the fetch logic now allows cancellation and re-start when the correct behavior is attached—even if no prior data exists—so the subsequent retry yields the properly transformed infinite structure.

Changes

In query-core, DehydratedQuery now includes an optional isInfiniteQuery flag, which dehydrateQuery sets for infinite queries and hydrate carries onto the Query instance. The Query class maintains this via a private #isInfiniteQuery field with corresponding accessors. The fetch path has been updated so hydrated infinite queries with a stuck retryer can transition cleanly once behavior is applied. In react-query, comprehensive tests exercise SSR hydration and recovery paths to verify that the infinite data shape is preserved across the boundary.

Tests

The new suite covers failed SSR hydration with subsequent client recovery, mixtures of infinite and regular queries, race conditions between hydration and observer attachment, and structural validation of { pages, pageParams }, including edge cases such as nested keys and multiple concurrent queries. All existing tests pass unchanged.

Example

// Server - fails
await queryClient.prefetchInfiniteQuery({
  queryKey: ['posts'],
  queryFn: () => throw new Error('Network error'),
  initialPageParam: 0,
  getNextPageParam: () => 1,
})

// Client - recovers with correct structure
const { data } = useSuspenseInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam }) => fetchPosts(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

// data is now correctly typed as { pages: [...], pageParams: [...] }

Notes

The change is backward-compatible and adds only negligible overhead, because the new flag is relevant solely for hydrated queries under the described conditions.

Summary by CodeRabbit

  • New Features

    • Hydration/dehydration now preserves infinite query state across cycles and JSON serialization.
    • InfiniteQueryObserver is now available in the public API for easier infinite-query handling.
  • Bug Fixes

    • Prevents stuck or unnecessary refetches for hydrated infinite queries with smarter cancellation logic.
    • Robust handling of failed server-side prefetches to avoid corrupting hydrated client state.
    • Correctly maintains behavior for mixed sets of infinite and regular queries, including nested keys.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Sep 9, 2025

Walkthrough

Adds isInfiniteQuery propagation through dehydration/hydration, exposes setters/getters on Query, and adjusts fetch-cancellation logic for hydrated infinite queries. Extends tests for hydration and SSR with infinite queries and adds a public export for InfiniteQueryObserver.

Changes

Cohort / File(s) Summary
Core hydration logic
packages/query-core/src/hydration.ts
Adds optional isInfiniteQuery on DehydratedQuery, sets it during dehydrate for infinite queries, and restores/marks queries as infinite during hydrate.
Query behavior and APIs
packages/query-core/src/query.ts
Adds private flag, getter, and setter for isInfiniteQuery; updates fetch cancellation to handle hydrated infinite queries (idle, retries, cancelRefetch).
Core tests: hydration
packages/query-core/src/__tests__/hydration.test.tsx
New tests validating isInfiniteQuery preservation across de/rehydration, JSON roundtrip, and mixed/nested keys.
React public API and SSR tests
packages/react-query/src/index.ts, packages/react-query/src/__tests__/ssr-hydration.test.tsx
Exports InfiniteQueryObserver; adds SSR hydration tests for infinite queries including failure, races, mixed queries, and successful paths.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App
  participant ServerCache as QueryClient (Server)
  participant Dehydrate as dehydrate()
  participant Hydrate as hydrate()
  participant ClientCache as QueryClient (Client)
  participant Query as Query

  App->>ServerCache: prefetchInfiniteQuery / prefetchQuery
  ServerCache->>Dehydrate: dehydrate()
  Dehydrate-->>App: dehydratedState (isInfiniteQuery flags)

  App->>Hydrate: hydrate(dehydratedState, ClientCache)
  Hydrate->>ClientCache: upsert queries
  Hydrate->>Query: setIsInfiniteQuery(true) when flagged
  Hydrate-->>App: client ready with flags preserved
Loading
sequenceDiagram
  autonumber
  participant Observer as InfiniteQueryObserver
  participant Query as Query
  participant Retryer as Retryer
  participant Source as Fetch Source

  Observer->>Query: fetch({ cancelRefetch })
  alt Hydrated infinite w/o data and cancelRefetch
    Query->>Retryer: check pending
    Query-->>Observer: cancel silently
  else Has data and cancelRefetch
    Query-->>Observer: cancel silently
  else Retry in progress
    Query->>Retryer: continue retries
    Retryer->>Source: refetch as configured
    Retryer-->>Observer: promise resolves/rejects
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

package: query-core, package: react-query

Suggested reviewers

  • TkDodo
  • manudeli

Pre-merge checks (4 passed, 1 warning)

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly and concisely summarizes the primary change by indicating the fix for preserving the infinite query type during failed SSR hydration, directly reflecting the core purpose of the pull request.
Linked Issues Check ✅ Passed The implementation introduces the isInfiniteQuery flag in dehydration and hydration flows, updates the fetch logic for pending retries on hydrated infinite queries, and adds focused tests, fully addressing the objectives described in [#8825].
Out of Scope Changes Check ✅ Passed All modifications, including hydration logic updates, query class enhancements, and new tests, remain tightly scoped to resolving the linked issue and introduce no unrelated functionality.
Description Check ✅ Passed The description is comprehensive and directly related to the changeset, outlining the problem, root cause, solution, and test coverage, which effectively communicates the intent and scope of the pull request.

Poem

I hopped through bytes with careful cheer,
Tagging queries far and near.
Infinite trails now marked just right,
So hydration’s glow returns at night.
No stuck retrace, no frantic hurry—
The pages load, so do not worry.
Thump-thump—ship it! 🐇✨

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Sep 9, 2025

View your CI Pipeline Execution ↗ for commit 909d953

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

☁️ Nx Cloud last updated this comment at 2025-09-09 09:34:39 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Sep 9, 2025

More templates

@tanstack/angular-query-devtools-experimental

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

@tanstack/angular-query-experimental

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

@tanstack/eslint-plugin-query

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

@tanstack/query-async-storage-persister

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

@tanstack/query-broadcast-client-experimental

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

@tanstack/query-core

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

@tanstack/query-devtools

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

@tanstack/query-persist-client-core

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

@tanstack/query-sync-storage-persister

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

@tanstack/react-query

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

@tanstack/react-query-devtools

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

@tanstack/react-query-next-experimental

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

@tanstack/react-query-persist-client

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

@tanstack/solid-query

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

@tanstack/solid-query-devtools

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

@tanstack/solid-query-persist-client

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

@tanstack/svelte-query

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

@tanstack/svelte-query-devtools

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

@tanstack/svelte-query-persist-client

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

@tanstack/vue-query

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

@tanstack/vue-query-devtools

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

commit: 909d953

@codecov
Copy link
Copy Markdown

codecov bot commented Sep 9, 2025

Codecov Report

❌ Patch coverage is 68.75000% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.63%. Comparing base (ccedf33) to head (909d953).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main    #9633       +/-   ##
===========================================
+ Coverage   45.50%   59.63%   +14.13%     
===========================================
  Files         209      138       -71     
  Lines        8377     5629     -2748     
  Branches     1897     1529      -368     
===========================================
- Hits         3812     3357      -455     
+ Misses       4118     1968     -2150     
+ Partials      447      304      -143     
Components Coverage Δ
@tanstack/angular-query-devtools-experimental ∅ <ø> (∅)
@tanstack/angular-query-experimental 87.00% <ø> (ø)
@tanstack/eslint-plugin-query ∅ <ø> (∅)
@tanstack/query-async-storage-persister 43.85% <ø> (ø)
@tanstack/query-broadcast-client-experimental 24.39% <ø> (ø)
@tanstack/query-codemods ∅ <ø> (∅)
@tanstack/query-core 97.22% <68.75%> (-0.26%) ⬇️
@tanstack/query-devtools 3.48% <ø> (ø)
@tanstack/query-persist-client-core 79.60% <ø> (ø)
@tanstack/query-sync-storage-persister 84.61% <ø> (ø)
@tanstack/query-test-utils ∅ <ø> (∅)
@tanstack/react-query 96.00% <ø> (ø)
@tanstack/react-query-devtools 10.00% <ø> (ø)
@tanstack/react-query-next-experimental ∅ <ø> (∅)
@tanstack/react-query-persist-client 100.00% <ø> (ø)
@tanstack/solid-query 78.13% <ø> (ø)
@tanstack/solid-query-devtools ∅ <ø> (∅)
@tanstack/solid-query-persist-client 100.00% <ø> (ø)
@tanstack/svelte-query 87.58% <ø> (ø)
@tanstack/svelte-query-devtools ∅ <ø> (∅)
@tanstack/svelte-query-persist-client 100.00% <ø> (ø)
@tanstack/vue-query 71.10% <ø> (ø)
@tanstack/vue-query-devtools ∅ <ø> (∅)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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: 2

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

407-425: Stuck hydrated infinite guard: solid; consider a small robustness tweak

Logic correctly cancels the “pending retryer from hydration” when an infinite-aware refetch arrives with cancelRefetch: true. To make it resilient if callers omit the options argument, prefer the effective behavior source:

-      const isStuckHydratedInfinite =
-        this.#isInfiniteQuery &&
-        this.state.data === undefined &&
-        this.#retryer?.status() === 'pending' &&
-        options?.behavior &&
-        fetchOptions?.cancelRefetch
+      const effectiveBehavior = options?.behavior ?? this.options.behavior
+      const isStuckHydratedInfinite =
+        this.#isInfiniteQuery &&
+        this.state.data === undefined &&
+        this.#retryer?.status() === 'pending' &&
+        !!effectiveBehavior &&
+        fetchOptions?.cancelRefetch

This keeps the intent while handling fetch(undefined, { cancelRefetch: true }) call sites.

packages/react-query/src/__tests__/ssr-hydration.test.tsx (4)

271-333: Strengthen assertions, remove console.log, and clean up clients

  • Assert that the hydrated query is flagged infinite before refetch.
  • Drop console log noise.
  • Validate first page payload.
  • Clear clients to avoid cross-test leakage.
@@
-    const clientQueryClient = new QueryClient()
-    hydrate(clientQueryClient, dehydrated)
+    const clientQueryClient = new QueryClient()
+    hydrate(clientQueryClient, dehydrated)
+    // Ensure infinite semantics were preserved on hydration
+    const hydratedQuery = clientQueryClient
+      .getQueryCache()
+      .find({ queryKey: ['posts'] })
+    expect((hydratedQuery as any).isInfiniteQuery?.()).toBe(true)
@@
-    expect(() => {
-      const pageCount = initialResult.data?.pages?.length ?? 0
-      console.log('Initial page count:', pageCount)
-    }).not.toThrow()
+    expect(() => {
+      // Safe to touch .pages even if undefined
+      const _ = initialResult.data?.pages?.length ?? 0
+    }).not.toThrow()
@@
-    expect(afterRefetch.data?.pages).toHaveLength(1)
+    expect(afterRefetch.data?.pages).toHaveLength(1)
+    expect(afterRefetch.data?.pages[0]).toEqual({
+      posts: ['Post 1-1', 'Post 1-2'],
+      nextCursor: 2,
+    })
@@
     expect(afterNextPage.data?.pages[1]).toEqual({
       posts: ['Post 2-1', 'Post 2-2'],
       nextCursor: 3,
     })
+    clientQueryClient.clear()
+    serverClient.clear()

334-391: Add teardown to prevent state leakage across tests

Clear clients at the end to avoid retained caches/timers under fake timers.

@@
-    expect(result2.data?.pages[0]).toEqual({ data: 'user2-0' })
+    expect(result2.data?.pages[0]).toEqual({ data: 'user2-0' })
+    clientQueryClient.clear()
+    serverClient.clear()

418-467: Confirm infinite flag for mixed hydration and add teardown

Explicitly assert infinite type on the infinite query and clear clients.

@@
-    expect(infiniteResult.data?.pages).toBeDefined()
-    expect(regularResult).toBe('regular')
+    expect(infiniteResult.data?.pages).toBeDefined()
+    expect(regularResult).toBe('regular')
+    const infQuery = clientQueryClient
+      .getQueryCache()
+      .find({ queryKey: ['infinite'] })
+    expect((infQuery as any).isInfiniteQuery?.()).toBe(true)
+    clientQueryClient.clear()
+    serverClient.clear()

469-506: Assert infinite flag on success path and clean up

Add a direct check that hydration preserved infinite semantics and clear clients.

@@
-    expect(result.data?.pages[0]).toEqual({ data: 'page-0', next: 1 })
+    expect(result.data?.pages[0]).toEqual({ data: 'page-0', next: 1 })
+    const hydratedQuery = clientQueryClient
+      .getQueryCache()
+      .find({ queryKey: ['success'] })
+    expect((hydratedQuery as any).isInfiniteQuery?.()).toBe(true)
+    clientQueryClient.clear()
+    serverClient.clear()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ccedf33 and 909d953.

📒 Files selected for processing (4)
  • packages/query-core/src/__tests__/hydration.test.tsx (1 hunks)
  • packages/query-core/src/hydration.ts (4 hunks)
  • packages/query-core/src/query.ts (4 hunks)
  • packages/react-query/src/__tests__/ssr-hydration.test.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
packages/query-core/src/query.ts (1)
packages/query-core/src/queryObserver.ts (2)
  • options (377-383)
  • fetchOptions (334-351)
packages/query-core/src/hydration.ts (1)
packages/query-core/src/query.ts (1)
  • isInfiniteQuery (203-205)
packages/react-query/src/__tests__/ssr-hydration.test.tsx (1)
packages/query-core/src/hydration.ts (2)
  • dehydrate (124-165)
  • hydrate (167-283)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (12)
packages/query-core/src/hydration.ts (2)

44-55: Dehydrated payload extension looks good

Adding isInfiniteQuery?: boolean is backward-compatible and keeps JSON shape stable. No issues here.


108-109: Marking infinite queries during dehydration is correct, but relies on options.behavior heuristic

Using query.options.behavior is a good proxy for infinite queries in core. If other custom behaviors exist in your ecosystem, verify this won’t mislabel them as infinite.

Do you want a follow-up test that dehydrates a non-infinite custom behavior to ensure the flag remains unset?

packages/query-core/src/__tests__/hydration.test.tsx (6)

1403-1434: Great: preserves infinite type on failed promise hydration

Covers the regression path well.


1436-1467: Good: dehydration marks only infinite queries

Validates selective flagging; nice separation from regular queries.


1469-1499: Good: end-to-end flag preservation across hydration

Ensures round-trip correctness for failed infinite queries.


1500-1533: Good: JSON round-trip of the flag

Covers common SSR flow with serialization.


1534-1577: Good: no impact on regular queries

Prevents accidental behavior changes for non-infinite.


1578-1604: Good: nested keys scenario

Validates realistic key shapes.

packages/query-core/src/query.ts (3)

178-179: Private flag for infinite queries: OK

Internal-only storage is appropriate.


203-206: Getter is minimal and clear

Exposing as boolean | undefined is fine and matches tests.


257-260: Setter is fine

No side effects required; simple assignment works.

packages/react-query/src/__tests__/ssr-hydration.test.tsx (1)

6-6: Public InfiniteQueryObserver import looks good

Importing from the public index aligns tests with the intended API surface.

Comment on lines +201 to 212
({
queryKey,
state,
queryHash,
meta,
promise,
dehydratedAt,
isInfiniteQuery,
}) => {
const syncData = promise ? tryResolveSync(promise) : undefined
const rawData = state.data === undefined ? syncData?.data : state.data
const data = rawData === undefined ? rawData : deserializeData(rawData)
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.

🛠️ Refactor suggestion

Also set isInfiniteQuery on pre-existing queries during hydration

Right now, setIsInfiniteQuery(true) is only called when a new Query is built. If a Query already exists in the cache (e.g., user code created it before hydrate()), the flag isn’t applied, and the retry/cancel logic in Query.fetch won’t kick in for that edge case.

Apply the flag for both new and existing queries:

       ) 
-        )
-      
-        if (isInfiniteQuery) {
-          query.setIsInfiniteQuery(true)
-        }
-      }
+        )
+      }
+
+      if (isInfiniteQuery) {
+        query.setIsInfiniteQuery(true)
+      }

Also applies to: 259-262

🤖 Prompt for AI Agents
In packages/query-core/src/hydration.ts around lines 201-212 (and likewise
adjust lines 259-262), the hydration path only calls setIsInfiniteQuery(true)
when building a new Query, leaving pre-existing queries without the flag; update
the logic so that after resolving or obtaining the Query instance (both in the
new-query branch and the existing-query branch) you call
query.setIsInfiniteQuery(Boolean(isInfiniteQuery)) (or
query.setIsInfiniteQuery(isInfiniteQuery) if already boolean) so the flag is
applied regardless of whether the Query was created during hydration or already
existed in the cache.

Comment on lines +393 to +416
it('should handle regular query (non-infinite) after hydration', async () => {
const serverClient = new QueryClient({
defaultOptions: {
dehydrate: { shouldDehydrateQuery: () => true },
},
})

await serverClient
.prefetchQuery({
queryKey: ['regular'],
queryFn: () => Promise.reject(new Error('Failed')),
retry: false,
})
.catch(() => {})

const dehydrated = dehydrate(serverClient)
const clientQueryClient = new QueryClient()
hydrate(clientQueryClient, dehydrated)

const query = clientQueryClient
.getQueryCache()
.find({ queryKey: ['regular'] })
expect((query as any)?.__isInfiniteQuery).toBeUndefined()
})
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.

💡 Verification agent

🧩 Analysis chain

Fix meaningless assertion; verify via isInfiniteQuery() instead

__isInfiniteQuery does not exist and this test would pass even if the query were incorrectly marked. Assert via the getter.

@@
-    expect((query as any)?.__isInfiniteQuery).toBeUndefined()
+    // Verify via the getter rather than a non-existent slot
+    expect((query as any).isInfiniteQuery?.()).toBe(false)
+    clientQueryClient.clear()
+    serverClient.clear()

Run to confirm the getter exists on Query:


🏁 Script executed:

#!/bin/bash
rg -nP -C2 '\bisInfiniteQuery\s*\(' packages/query-core/src/query.ts

Length of output: 108


Replace __isInfiniteQuery check with the public isInfiniteQuery getter
Use the property (not a method) to assert the query type and clean up clients:

-    expect((query as any)?.__isInfiniteQuery).toBeUndefined()
+    expect((query as any).isInfiniteQuery).toBe(false)
+    clientQueryClient.clear()
+    serverClient.clear()
📝 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
it('should handle regular query (non-infinite) after hydration', async () => {
const serverClient = new QueryClient({
defaultOptions: {
dehydrate: { shouldDehydrateQuery: () => true },
},
})
await serverClient
.prefetchQuery({
queryKey: ['regular'],
queryFn: () => Promise.reject(new Error('Failed')),
retry: false,
})
.catch(() => {})
const dehydrated = dehydrate(serverClient)
const clientQueryClient = new QueryClient()
hydrate(clientQueryClient, dehydrated)
const query = clientQueryClient
.getQueryCache()
.find({ queryKey: ['regular'] })
expect((query as any)?.__isInfiniteQuery).toBeUndefined()
})
it('should handle regular query (non-infinite) after hydration', async () => {
const serverClient = new QueryClient({
defaultOptions: {
dehydrate: { shouldDehydrateQuery: () => true },
},
})
await serverClient
.prefetchQuery({
queryKey: ['regular'],
queryFn: () => Promise.reject(new Error('Failed')),
retry: false,
})
.catch(() => {})
const dehydrated = dehydrate(serverClient)
const clientQueryClient = new QueryClient()
hydrate(clientQueryClient, dehydrated)
const query = clientQueryClient
.getQueryCache()
.find({ queryKey: ['regular'] })
expect((query as any).isInfiniteQuery).toBe(false)
clientQueryClient.clear()
serverClient.clear()
})
🤖 Prompt for AI Agents
In packages/react-query/src/__tests__/ssr-hydration.test.tsx around lines 393 to
416, replace the internal check using __isInfiniteQuery with the public
isInfiniteQuery getter on the Query instance (e.g.,
expect(query?.isInfiniteQuery).toBeUndefined()) and ensure test cleans up
created QueryClient instances by clearing/destroying serverClient and
clientQueryClient after use to avoid leaks.

@TkDodo
Copy link
Copy Markdown
Collaborator

TkDodo commented Sep 9, 2025

serializing the information about “infinite” query seems fine, though I think we probably shouldn’t do this with a flag, but maybe with a type like field that would allow more than just two behvaviors.

what I don’t understand is why it isn’t enough to just set options.behavior to infiniteQueryBehavior before calling query.fetch in the hydration, as I’ve outlined here:

this should be enough, and should be the solution we strive towards.

@joseph0926
Copy link
Copy Markdown
Contributor Author

serializing the information about “infinite” query seems fine, though I think we probably shouldn’t do this with a flag, but maybe with a type like field that would allow more than just two behvaviors.

what I don’t understand is why it isn’t enough to just set options.behavior to infiniteQueryBehavior before calling query.fetch in the hydration, as I’ve outlined here:

this should be enough, and should be the solution we strive towards.

You're absolutely right.
I'll follow the design you suggested and apply the solution appropriately now.
Thanks for the feedback.

@TkDodo
Copy link
Copy Markdown
Collaborator

TkDodo commented Sep 9, 2025

You're absolutely right.
I'll follow the design you suggested and apply the solution appropriately now.

Sorry this sounds absolutely like something that claude code would say. I have nothing against using AI as an assistant but please let me know if everything you do is just coming straight from AI and this account is just a proxy for it.

@joseph0926
Copy link
Copy Markdown
Contributor Author

You're absolutely right.
I'll follow the design you suggested and apply the solution appropriately now.

Sorry this sounds absolutely like something that claude code would say. I have nothing against using AI as an assistant but please let me know if everything you do is just coming straight from AI and this account is just a proxy for it.

That's how Deepl translates.
Try translating “당신의 말이 맞습니다” into English.
Of course, AI like GPT is used, but calling it an “if everything you do is just coming straight from AI and this account is just a proxy for it” seems a bit of an overstatement.

@joseph0926
Copy link
Copy Markdown
Contributor Author

joseph0926 commented Sep 9, 2025

I’m not a native English speaker,
so I sometimes use translation and assistive tools.
Hearing that “everything I do is coming straight from AI” and that this account is “just a proxy for it”—especially based on my use of the phrase “You’re absolutely right”—was disappointing,
Honestly, if I'd been asked questions like "Do you code with AI?"
I would have admitted yes or no, or explained something,
but when I hear "Are you AI?" I can't say anything at all...
but I’m sorry if my wording made you uncomfortable.
I sincerely hope the project continues to improve and that remaining bugs are addressed thoroughly, whether with the help of AI tools or otherwise.
Since it seems further contributions from me may not be welcome, I’ll refrain from submitting more for now, but I’ll continue to use and support the project as a user.

But I learned a lot from you during my time contributing, and it was truly enjoyable.
Thank you.

@joseph0926 joseph0926 closed this Sep 9, 2025
@joseph0926 joseph0926 deleted the fix/prfetch-infinite-hydration branch September 9, 2025 10:44
@joseph0926
Copy link
Copy Markdown
Contributor Author

@TkDodo
I respect your concern. and once again, I'm sorry if this caused you any discomfort.
However, no matter how much I think about it, I simply cannot figure it out, so I respectfully ask a question.
From my perspective, it's not clear how that single sentence led to the conclusion of 'proxy'.
Could you point out the exact phrasing that made you uncomfortable? I'm genuinely asking so I can make corrections, whether in this PR or elsewhere.
I apologize for taking up your time when you must be busy.
However, from my perspective, that expression was shocking, so I'm asking about it.

@TkDodo
Copy link
Copy Markdown
Collaborator

TkDodo commented Sep 9, 2025

Let me start by saying that I’m very sorry for how my comment came across. I too am not a native speaker, so I understand the troubles. Re-reading my comment now it’s clear to me that it was inappropriate.

Honestly, if I'd been asked questions like "Do you code with AI?"

Yes, you’re right, that would’ve been the better question, and I should’ve phrased it that way.

What I found odd in this interaction is that I tried to outline in the linked issue what I think the problem is and what a potential solution could look like. Then, this PR shows up that implements a different approach, which I think would fail in the normal case of a successfully streamed infinite query because it is absolutely necessary that we attach the infinite query behavior in all cases. So when I point out pretty much the exact same thing that I said on the issue again in the PR, I’m met with “you’re absolutely right”. This is typical for AI interactions: One says something, AI does something else, you say the exact same thing again, and get an “you’re absolutely right, thanks for pointing that out. I will fix it that way” response.

That gave me the feeling that the whole conversation is being held by AI, and that I might be wasting my time here. However, I shouldn’t have written that unreflected comment, and again, I’m sorry for that.

@joseph0926
Copy link
Copy Markdown
Contributor Author

joseph0926 commented Sep 9, 2025

@TkDodo
Thank you for your thoughtful reply. After reading your additional explanation, I understand the source of the misunderstanding and your concerns.

To explain the reasons behind your concerns, I did read your comment on the issue. However, I became overly focused on this specific part

“the conceptual problem I’m having now is that hydration doesn’t know about infinite / non-infinite query. There’s no information stored in the cache that knows about this.”

Because I fixated on that line, I framed the problem as “if we just solve this piece, the whole thing will work,” and I proceeded from that assumption.

Approaching it that way led me down a different path from your proposed solution. After receiving your feedback and re-reading your comment carefully, I realized that my direction was aimed at addressing a specific symptom, while your approach both resolves that symptom and tackles the root cause.

That is why I said I would make changes in response to your feedback.

Having read your detailed explanation, I fully understand the concern and how the misunderstanding arose.

Thank you again for taking the time to explain everything so clearly. I was a bit emotional as well, I sincerely apologize for that.

Thanks again.

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