Skip to content

fix(query-core): resolve suspense when query data is set programmatically#11036

Open
AmariahAK wants to merge 3 commits into
TanStack:mainfrom
AmariahAK:main
Open

fix(query-core): resolve suspense when query data is set programmatically#11036
AmariahAK wants to merge 3 commits into
TanStack:mainfrom
AmariahAK:main

Conversation

@AmariahAK

@AmariahAK AmariahAK commented Jul 8, 2026

Copy link
Copy Markdown

PR Description

When `useSuspenseQuery` is used with `streamedQuery` or `setQueryData`,
the Suspense boundary stays stuck on the original fetch promise even
after data arrives in the cache. This happens because `fetchOptimistic`
in `queryObserver.ts` returned `query.fetch().then(...)` — a promise
that only resolves when the `queryFn` completes, ignoring cache updates
from other sources.

**Fix:** Modified `fetchOptimistic` to use `Promise.race` between the
fetch promise and a query cache subscriber. The subscriber listens for
`'updated'` events on the same query hash and resolves immediately when
`query.state.data` becomes defined — whether via `setQueryData`, a
streamed chunk, or any other mechanism. The fetch continues running
in the background; only the suspense block is released early.

**Why `Promise.race` over alternatives:**
- Resolving the fetch promise early would abort the ongoing fetch
  (breaks streamed queries' abort signal — the approach attempted in
  preview PR #10994)
- Switching to an observer-based throw in `useBaseQuery` would add
  undue complexity
- `Promise.race` with the existing cache notification system is a
  20-line diff that touches nothing else

**Files changed:**

| File | Change |
|------|--------|
| `packages/query-core/src/queryObserver.ts` | +20/−1 in `fetchOptimistic` |
| `packages/react-query/src/__tests__/useSuspenseQuery.test.tsx` | +102 lines (3 new tests) |
| `.changeset/resolve-suspense-setquerydata.md` | changeset (patch) |

**New tests:**
1. `setQueryData` called while fetch is in-flight → suspense releases
2. `streamedQuery` first chunk → suspense releases
3. `setQueryData` before component mounts → suspense never shows

## ✅ Checklist

- [x] I have followed the steps in the
  [Contributing guide](https://github.com/TanStack/query/blob/main/CONTRIBUTING.md).
- [x] I have tested this code locally with `pnpm run test:pr`.

@tanstack/query-core:test:lib → 546 passed, 0 failures
@tanstack/react-query:test:lib → 567 passed, 0 failures


## 🚀 Release Impact

- [x] This change affects published code, and I have generated a
  [changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).
- [ ] This change is docs/CI/dev-only (no release).

Closes #10924

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Bug Fixes**
  * Improved React Suspense behavior when query data is provided programmatically (for example, via manual cache updates) while a request is still in flight, so cached data can render immediately.
  * Ensured Suspense releases promptly when streamed/experimental streamed query data becomes available.
  * Preloaded cache data now avoids unnecessary initial suspension.
* **Tests**
  * Added/updated Suspense tests for manual cache updates during in-flight fetches, streamed first-chunk release, and preloaded-cache mounting behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

…ally

When useSuspenseQuery is used and setQueryData populates the cache
while a fetch is in-flight, the suspense boundary stays stuck on the
original fetch promise. This prevents streamedQuery and manual cache
updates from releasing suspense.

Modify fetchOptimistic in queryObserver to use Promise.race between
the fetch promise and a cache subscriber that listens for 'updated'
events. When data appears in the cache via setQueryData or a streamed
chunk, the race resolves immediately without aborting the fetch.
This allows the suspense boundary to release and show the available
data while the fetch continues in the background.

Closes TanStack#10924

Co-authored-by: atlarix-agent <agent@atlarix.dev>
Co-authored-by: atlarix-agent <agent@atlarix.dev>
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 50312ea8-4161-49ba-8f0b-9bdf6a5abed9

📥 Commits

Reviewing files that changed from the base of the PR and between 60a34d7 and 8bf5144.

📒 Files selected for processing (1)
  • packages/query-core/src/queryObserver.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/query-core/src/queryObserver.ts

📝 Walkthrough

Walkthrough

fetchOptimistic now resolves against cache updates as well as fetch completion, so Suspense can release when query data is set programmatically or streamed into the cache. Tests cover in-flight updates, streamed first chunks, and preloaded cache rendering, and a changeset records the patch release.

Changes

Suspense Resolution Fix

Layer / File(s) Summary
fetchOptimistic race implementation
packages/query-core/src/queryObserver.ts
Replaces the single query.fetch().then(...) path with a Promise.race between fetch completion and a cache subscription resolving on an updated event once query.state.data is defined.
Suspense release tests and changeset
packages/react-query/src/__tests__/useSuspenseQuery.test.tsx, .changeset/resolve-suspense-setquerydata.md
Adds tests for setQueryData during in-flight fetch, experimental_streamedQuery first-chunk release, and preloaded-cache mount, plus a patch changeset documenting the fix.

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

Sequence Diagram(s)

sequenceDiagram
  participant QueryObserver
  participant QueryFn
  participant QueryCache
  QueryObserver->>QueryFn: query.fetch()
  QueryObserver->>QueryCache: subscribe(updated)
  alt cache data arrives first
    QueryCache-->>QueryObserver: updated event with data
    QueryObserver->>QueryCache: unsubscribe()
    QueryObserver-->>QueryObserver: createResult()
  else fetch completes first
    QueryFn-->>QueryObserver: fetch resolved
    QueryObserver->>QueryCache: unsubscribe()
    QueryObserver-->>QueryObserver: createResult()
  end
Loading

Possibly related PRs

  • TanStack/query#10759: Both PRs modify the query “optimistic” path and interact with Suspense-related query behavior.

Suggested labels: package: query-core, package: react-query

Suggested reviewers: TkDodo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: resolving Suspense when query data is set programmatically.
Description check ✅ Passed The description follows the template, explains the fix, includes checklist items, tests, and release impact.
Linked Issues check ✅ Passed The code and tests address #10924 by releasing Suspense when cache data arrives via setQueryData or streamed chunks.
Out of Scope Changes check ✅ Passed The changes are limited to the Suspense fix, related tests, and a changeset, with no obvious unrelated additions.
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.

@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


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0f9c9f6e-0fdd-44e8-b976-3179c1f4098f

📥 Commits

Reviewing files that changed from the base of the PR and between 64241b8 and 60a34d7.

📒 Files selected for processing (3)
  • .changeset/resolve-suspense-setquerydata.md
  • packages/query-core/src/queryObserver.ts
  • packages/react-query/src/__tests__/useSuspenseQuery.test.tsx

Comment thread packages/query-core/src/queryObserver.ts
…h rejection

unsubscribe() was only called in the .then() success path. If
query.fetch() rejected before the cache subscriber resolved the race,
the subscription leaked — staying registered on QueryCache indefinitely
and checking a stale queryHash on every 'updated' event.

Use .finally() to ensure unsubscribe() runs on both success and
rejection, preserving the original promise type through the chain.

Co-authored-by: atlarix-agent <agent@atlarix.dev>
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