Skip to content

fix(query-core): use a hydrated query's current 'retry' options when it rejects - #1

Closed
KATT wants to merge 7 commits into
masterfrom
cursor/hydrated-rejection-retry-271e
Closed

KATT wants to merge 7 commits into
masterfrom
cursor/hydrated-rejection-retry-271e

Conversation

@KATT

@KATT KATT commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

🎯 Changes

When dehydrate includes a pending query, hydrate resumes its promise on the client. That fetch starts before any observer exists, so its retryer took retry and retryDelay from the options the query had at hydration. If useQuery mounted with retry: false, or a different retryDelay, before the server promise rejected, the hydration options still decided whether and when the query retried.

Now, for fetches that resume a hydrated promise (initialPromise), the retryer reads both options at each failure:

  • If a mounted observer set retry or retryDelay itself, that value wins.
  • Otherwise the retryer uses the options the fetch started with, as on master. Client defaults (defaultOptions.queries) and setQueryDefaults don't count as set by the observer, so they don't override hydrate.queries values. For example, a client with queries.retry: 2 that hydrates with hydrate.queries.retry: 0 still fails at once.

useQuery merges client defaults into its options before it creates the observer, so the observer can't tell which values the caller set. queryClient.defaultQueryOptions therefore records the retry and retryDelay it was called with, in a module-level WeakMap keyed by the options object it returns. Options objects gain no new properties, and nothing else reads the map.

Infinite queries get the same fix. infiniteQueryBehavior used to build every retry of a hydrated infinite query from the options the fetch started with, which have no queryFn, so each retry replayed the rejected promise. When the fetch resumes a hydrated promise, a retry now reads queryFn, initialPageParam, getNextPageParam and the other page options from the query's current options. With an observer mounted, it runs the observer's queryFn from the first page, as plain queries already did. With no observer, it replays the rejection as before. Whether it retries at all follows the rules above.

A fetch that joins a running fetch gets that fetch's shared promise (TanStack#12489). When the running fetch resumes a hydrated promise, the shared promise resolves with the data of the retry the observer configured.

Every other fetch keeps the options it started with, and a new test in query.test.tsx checks that.

Out of scope: networkMode still comes from the options the fetch started with, as on master.

New tests in hydration.test.tsx:

  • the retry uses the observer's retry and retryDelay, and runs the observer's queryFn
  • retry: false on an observer stops retries
  • the query keeps the hydration options that the observer doesn't set
  • the hydration options win over the client's query defaults
  • the hydration options win over setQueryDefaults
  • retry options the observer sets itself win over the client's query defaults
  • the query uses the hydration options when no observer mounts (as before)
  • a fetch that joins the hydrated promise resolves with the data of the observer's retry
  • a hydrated infinite query retries with the observer's queryFn from the first page
  • a hydrated infinite query with no observer replays the rejection under the hydration options (as before)

The new tests clean up with using: using _timers = fakeTimers() switches timers, using queryClient = clearOnDispose(new QueryClient()) clears each client, and using _subscription = subscribe(observer, noop) unsubscribes each observer. This PR adds all three helpers, with tests, to @tanstack/query-test-utils. The existing hooks in these files stay as they were.

This PR doesn't depend on #2, and they share no code. #2 adds the same three helpers byte for byte, so whichever merges second only has to merge the export list in query-test-utils/src/index.ts.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested code changes locally with pnpm run test:pr, or these tests do not apply to this pull request. (Ran oxlint, knip, test:types across all TS versions and vitest for query-core, query-test-utils and react-query, plus the docs link check.)
  • I have followed the AI contribution policy and fully understand the code in this pull request, including any code generated with AI assistance.

🚀 Release Impact

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

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

🚀 Changeset Version Preview

1 package(s) bumped directly, 23 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/query-core 5.105.2 → 5.105.3 Changeset
@tanstack/angular-query-experimental 5.105.2 → 5.105.3 Dependent
@tanstack/eslint-plugin-query 5.105.2 → 5.105.3 Dependent
@tanstack/lit-query 0.2.33 → 0.2.34 Dependent
@tanstack/preact-query 5.105.2 → 5.105.3 Dependent
@tanstack/preact-query-devtools 5.105.2 → 5.105.3 Dependent
@tanstack/preact-query-persist-client 5.105.2 → 5.105.3 Dependent
@tanstack/query-async-storage-persister 5.105.2 → 5.105.3 Dependent
@tanstack/query-broadcast-client-experimental 5.105.2 → 5.105.3 Dependent
@tanstack/query-devtools 5.105.2 → 5.105.3 Dependent
@tanstack/query-persist-client-core 5.105.2 → 5.105.3 Dependent
@tanstack/query-sync-storage-persister 5.105.2 → 5.105.3 Dependent
@tanstack/react-query 5.105.2 → 5.105.3 Dependent
@tanstack/react-query-devtools 5.105.2 → 5.105.3 Dependent
@tanstack/react-query-next-experimental 5.105.2 → 5.105.3 Dependent
@tanstack/react-query-persist-client 5.105.2 → 5.105.3 Dependent
@tanstack/solid-query 5.105.2 → 5.105.3 Dependent
@tanstack/solid-query-devtools 5.105.2 → 5.105.3 Dependent
@tanstack/solid-query-persist-client 5.105.2 → 5.105.3 Dependent
@tanstack/svelte-query 6.3.8 → 6.3.9 Dependent
@tanstack/svelte-query-devtools 6.3.8 → 6.3.9 Dependent
@tanstack/svelte-query-persist-client 6.3.8 → 6.3.9 Dependent
@tanstack/vue-query 5.105.2 → 5.105.3 Dependent
@tanstack/vue-query-devtools 6.3.8 → 6.3.9 Dependent

@cursor cursor Bot changed the title fix(query-core): retry a hydrated query that rejects with its current 'retry' options fix(query-core): use a hydrated query's current 'retry' options when it rejects Nov 2, 2026
cursoragent and others added 7 commits November 2, 2026 19:53
… 'retry' options

A query hydrated while pending resumes the dehydrated promise through a
retryer created at hydration time, before any observer has set the
query's options. Its retries used the 'retry' and 'retryDelay' the query
had then, so an observer's 'retry: false' or 'retryDelay' was ignored.

Read 'retry' and 'retryDelay' from the query's options at the time of
each failure for fetches that resume an 'initialPromise'. Other fetches
keep the options they started with.

Co-authored-by: alexander <alexander@n1s.se>
… with 'using'

Co-authored-by: alexander <alexander@n1s.se>
… with 'using'

Adds a 'subscribe' helper to @tanstack/query-test-utils that returns a Disposable, and a test for it and for 'clearOnDispose'.

Co-authored-by: alexander <alexander@n1s.se>
Co-authored-by: alexander <alexander@n1s.se>
…ions

Co-authored-by: alexander <alexander@n1s.se>
…de hydration

Client defaults and setQueryDefaults no longer count as set, so they
don't override hydrate.queries.retry.

Co-authored-by: alexander <alexander@n1s.se>
…the observer's retry

Co-authored-by: alexander <alexander@n1s.se>
@cursor
cursor Bot force-pushed the cursor/hydrated-rejection-retry-271e branch from 3ff29ca to 9e4a941 Compare November 2, 2026 19:56
@KATT

KATT commented Nov 3, 2026

Copy link
Copy Markdown
Owner Author

Moved to TanStack#12511.

@KATT KATT closed this Nov 3, 2026
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.

2 participants