Skip to content

fix(solid-query): don't re-trigger Suspense on background refetches - #11230

Open
brenelz wants to merge 1 commit into
TanStack:mainfrom
brenelz:fix/solid-suspense-background-refetch
Open

fix(solid-query): don't re-trigger Suspense on background refetches#11230
brenelz wants to merge 1 commit into
TanStack:mainfrom
brenelz:fix/solid-suspense-background-refetch

Conversation

@brenelz

@brenelz brenelz commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Background refetches (and mounts with cached data) no longer flip enclosing <Suspense> boundaries. This fixes CSS animations restarting, and focus/scroll/iframe state resetting, on every query update.

Fixes #9955
Related: #9883, and the downstream report TanStack/router#8000 (solid-router wraps every route match in <Suspense>, so route components can't escape a boundary — which made this present as a router bug).

Root cause

useBaseQuery's client subscriber calls the resource's refetch() on every observer notification, and the fetcher always returned a Promise — even when it resolved within the same tick (!isLoadingresolve() called synchronously in the executor). A native Promise's .then fires a microtask later, so the resource spent at least one microtask in a pending/refreshing state with pr set.

Any query.data read that ever registered with a SuspenseContext while pr was set leaves behind a tracking computed that re-runs on every trigger() and increments the boundary again (the .latest fast path can't prevent this: on mount with cached data, resolved is still false during that first microtask, so the initial read registers anyway — and structural sharing keeps data referentially stable, so the reading memo never re-runs and never disposes the computed).

The boundary flips to fallback and back within one microtask. That's never painted, but Suspense detaches and re-inserts the children's DOM — and reconnecting an element restarts its CSS animations. Verified in the router#8000 repro with a MutationObserver: the exact same DOM node is removed and re-inserted 1 ms apart precisely when a background refetch settles.

The existing mitigations in #10053 / #10592 gate on isFetching === false, so the stale-mount case (refetch already in flight at mount) still flips.

Fix

When the observer result has data and is not in an initial loading state, the fetcher returns the result synchronously (a non-thenable). Solid's load() completes non-Promise results immediately without ever setting a pending promise, so Suspense is only triggered by genuine initial loads. Two exceptions preserve existing semantics:

  • If a previous fetcher promise is still pending (resolver set — an in-flight initial load, where the boundary is already suspended and possibly inside a transition), completion goes through the Promise path so Suspense/Transition bookkeeping resolves correctly.
  • No-data results (e.g. disabled queries) keep the previous Promise behavior; there is no rendered content to protect and existing tests rely on the fallback window.

Behavior change

should refetch when re-mounting previously asserted that the Suspense fallback shows when re-mounting a component whose query has cached data. That expectation codified the reported bug; the test now expects cached data to render immediately while the mount refetch runs in the background (matching React useSuspenseQuery semantics). The refetch itself still happens and is still asserted.

Validation

  • 2 new regression tests: mounting with cached data during a background refetch, and invalidation-triggered refetch of a mounted query — both assert the fallback never renders and DOM node identity is preserved
  • Full solid-query runtime suite: 322 passed (baseline on main: 320), no new failures; transition test passes
  • End-to-end: built package swapped into the router#8000 repro — the same-node detach/re-insert at refetch completion is gone and the animation plays once

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved Suspense behavior when displaying cached data.
    • Background refetches and query invalidations no longer unnecessarily re-trigger Suspense or detach and remount content.
    • Initial loads without available data continue to suspend as expected.
    • Improved error handling and hydration behavior across server and client rendering.

Every query observer update was funneled through the underlying
resource's refetch(), whose fetcher always returned a Promise. Even a
Promise that resolves within the same tick leaves the resource in a
pending/refreshing state for at least a microtask, so every enclosing
<Suspense> boundary flipped to its fallback and back. The flip is never
painted, but it detaches and re-inserts the boundary's DOM, restarting
CSS animations and resetting focus/scroll/iframe state on every
background refetch and on every mount with cached data.

The fetcher now returns the observer result synchronously (a
non-thenable) when data is available and the query is not in an initial
loading state — Solid's resource completes such loads without ever
entering a pending state, so Suspense is only triggered by genuine
initial loads. Two exceptions preserve existing semantics: an in-flight
initial load (pending resolver) still completes through the Promise
path so Suspense/Transition bookkeeping resolves correctly, and
no-data results (e.g. disabled queries) keep their previous behavior.

Also updates the re-mount test, which codified the old
suspend-on-remount-with-cached-data behavior; cached data now renders
immediately while the mount refetch runs in the background, matching
React's useSuspenseQuery semantics.

Fixes TanStack#9955
Related: TanStack#9883, TanStack/router#8000

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 18, 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 Plus

Run ID: 64b178e6-1ab7-4195-a137-6c4c49d19adb

📥 Commits

Reviewing files that changed from the base of the PR and between b8e3559 and cb08a8f.

📒 Files selected for processing (3)
  • .changeset/solid-suspense-background-refetch.md
  • packages/solid-query/src/__tests__/suspense.test.tsx
  • packages/solid-query/src/useBaseQuery.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The Solid Query resource fetcher now resolves available client data synchronously when no fetch is pending. Server hydration and error handling remain promise-based. Suspense tests cover cached mounts, remounts, and invalidation refetches.

Changes

Solid Suspense resolution

Layer / File(s) Summary
Synchronous resource resolution
packages/solid-query/src/useBaseQuery.ts
The resource fetcher separates server and client execution. Client results with available data resolve synchronously when no fetch is pending. Initial loading and pending fetches continue through promises.
Suspense regression coverage
packages/solid-query/src/__tests__/suspense.test.tsx, .changeset/solid-suspense-background-refetch.md
Tests verify that cached data remains rendered during remounts, background refetches, and invalidation. The changeset documents the patch release.

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

Merge Risk: ⚪ Minimal · up to cb08a

The change keeps cached data rendered during background refetches so Suspense boundaries no longer reset UI state; no actionable merge-blocking risk remains beyond normal checks and review.

Possibly related PRs

  • TanStack/query#11036: Both changes address Suspense resolution with cached query data through different mechanisms.
  • TanStack/query#11105: Both changes update Solid Suspense tests for cached-query refetch behavior.
  • TanStack/query#11221: Both changes modify useBaseQuery.ts and Solid Suspense behavior.

Suggested labels: package: solid-query

Suggested reviewers: tkdodo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.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 clearly and concisely identifies the main fix: preventing Solid Suspense from re-triggering during background refetches.
Description check ✅ Passed The description clearly explains the change, root cause, behavior, validation, and release impact, although it omits the template checklist headings.
Linked Issues check ✅ Passed The implementation addresses issue [#9955] by returning cached query results synchronously and preventing unnecessary Suspense triggering after prior data loading.
Out of Scope Changes check ✅ Passed The code, regression tests, and changeset directly support the linked issue and stated objective, with no unrelated changes identified.
✨ 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.

@nx-cloud

nx-cloud Bot commented Aug 18, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit cb08a8f

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

☁️ Nx Cloud last updated this comment at 2026-08-18 22:56:30 UTC

@pkg-pr-new

pkg-pr-new Bot commented Aug 18, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-query-experimental

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

@tanstack/eslint-plugin-query

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

@tanstack/lit-query

npm i https://pkg.pr.new/@tanstack/lit-query@11230

@tanstack/preact-query

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

@tanstack/preact-query-devtools

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

@tanstack/preact-query-persist-client

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

@tanstack/query-async-storage-persister

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

@tanstack/query-broadcast-client-experimental

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

@tanstack/query-core

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

@tanstack/query-devtools

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

@tanstack/query-persist-client-core

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

@tanstack/query-sync-storage-persister

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

@tanstack/react-query

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

@tanstack/react-query-devtools

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

@tanstack/react-query-next-experimental

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

@tanstack/react-query-persist-client

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

@tanstack/solid-query

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

@tanstack/solid-query-devtools

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

@tanstack/solid-query-persist-client

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

@tanstack/svelte-query

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

@tanstack/svelte-query-devtools

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

@tanstack/svelte-query-persist-client

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

@tanstack/vue-query

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

@tanstack/vue-query-devtools

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

commit: cb08a8f

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.

Solid Suspense is triggered on data access after checking isSuccess when loaded by ensureQueryData

1 participant