fix: support async Svelte
๐ฏ Changes
Svelte 5 now supports async await in components. One behavior there is that when a component is created as part of a boundary that has pending async work, its $effects will not run until that async work is done.
The way the code is written right now means you can end up in an infinite pending state: If you do await someQuery.promise, the $effect for the subscription will never run, and therefore the await will never resolve. This PR fixes it.
โ Checklist
- [x] I have followed the steps in the Contributing guide.
- [x] I have tested this code locally with
pnpm run test:pr.
๐ Release Impact
- [x] This change affects published code, and I have generated a changeset.
- [ ] This change is docs/CI/dev-only (no release).
Summary by CodeRabbit
- Bug Fixes
- Restored reliable support for async Svelte so queries behave correctly in modern Svelte environments.
- Improved query lifecycle and subscription handling to ensure consistent result updates and proper cleanup across server and client lifecycles.
๐ฆ Changeset detected
Latest commit: 59a1acc5697b56e3c73296dd245b3b36b9fff949
The changes in this PR will be included in the next version bump.
This PR includes changesets to release 3 packages
| Name | Type |
|---|---|
| @tanstack/svelte-query | Patch |
| @tanstack/svelte-query-devtools | Patch |
| @tanstack/svelte-query-persist-client | 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
Walkthrough
Replaces the previous $effect-based subscription in createBaseQuery.svelte.ts with an eagerly-initialized, SSR-aware unsubscribe and a watchChanges re-subscription path that calls observer.updateResult() when needed; adds onDestroy cleanup. Adds a patch changeset entry for the release.
Changes
| Cohort / File(s) | Change Summary |
|---|---|
Changeset entry โ.changeset/thin-bugs-change.md |
Adds a patch changeset for @tanstack/svelte-query documenting: "fix: support async Svelte". No code/API changes in that file. |
Lifecycle / subscription refactor packages/svelte-query/src/createBaseQuery.svelte.ts |
Replaces $effect-based subscription with eager, SSR/client-aware unsubscribe initialization; introduces watchChanges to re-subscribe when isRestoring or observer change, calls observer.updateResult() on re-subscription, and registers onDestroy cleanup with try-catch for non-component contexts. |
Sequence Diagram
sequenceDiagram
participant Component as Svelte Component
participant WatchChanges as watchChanges
participant Observer
participant OnDestroy as onDestroy
rect rgb(245,245,255)
Note over Component,Observer: Previous ($effect) pattern
Component->>Observer: $effect establishes subscription
Observer-->>Component: returns unsubscribe
end
rect rgb(235,250,235)
Note over Component,WatchChanges: New pattern (eager init + watchChanges + onDestroy)
Component->>Observer: initialize unsubscribe (eager, SSR-aware)
Component->>WatchChanges: observe isRestoring / observer changes
alt Re-subscribe needed
WatchChanges->>Observer: call observer.updateResult()
WatchChanges->>Observer: subscribe -> return new unsubscribe
end
Component->>OnDestroy: register cleanup
OnDestroy->>Observer: call unsubscribe on destroy (safe try/catch)
end
Estimated code review effort
๐ฏ 4 (Complex) | โฑ๏ธ ~50 minutes
- Inspect SSR vs client branching and restoration-state logic.
- Verify unsubscribe re-initialization correctness and edge cases where
observer.updateResult()must run. - Check the try-catch on
onDestroyto ensure no silent failures in non-component contexts.
Suggested reviewers
- TkDodo
Poem
๐ฐ I twitch my whiskers at subscriptions new,
I re-subscribe and wake the watcher too,
I guard the teardown with a careful sigh,
And hop away clean when onDestroy says goodbye. ๐ฅ
Pre-merge checks and finishing touches
โ Passed checks (3 passed)
| Check name | Status | Explanation |
|---|---|---|
| Title Check | โ Passed | The PR title "fix: support async Svelte" directly aligns with the main objective of the changeset and code modifications. The raw summary confirms the change adds "fix support for async Svelte," and the PR description details the specific problem with Svelte 5 async components where $effect callbacks are deferred. The title is concise, uses the conventional "fix:" prefix, and clearly communicates the primary purpose to a developer reviewing history without requiring them to read the full description. |
| Description Check | โ Passed | The PR description comprehensively follows the repository template with all required sections properly completed. The Changes section provides detailed context about the Svelte 5 async behavior issue and explains why the fix is necessary. The Checklist section confirms both contributing guide compliance and local testing have been completed. The Release Impact section correctly indicates this change affects published code with a changeset generated, aligning with the raw summary showing a patch version bump entry. |
| Docstring Coverage | โ Passed | Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. |
โจ Finishing touches
- [ ] ๐ Generate docstrings
๐งช Generate unit tests (beta)
- [ ] Create PR with unit tests
- [ ] Post copyable unit tests in a comment
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.
Comment @coderabbitai help to get the list of available commands and usage tips.
View your CI Pipeline Execution โ for commit 59a1acc5697b56e3c73296dd245b3b36b9fff949
| Command | Status | Duration | Result |
|---|---|---|---|
nx affected --targets=test:sherif,test:knip,tes... |
โ Succeeded | 1m 13s | View โ |
nx run-many --target=build --exclude=examples/*... |
โ Succeeded | 6s | View โ |
โ๏ธ Nx Cloud last updated this comment at 2025-11-01 19:52:59 UTC
More templates
- @tanstack/query-example-angular-auto-refetching
- @tanstack/query-example-angular-basic
- @tanstack/query-example-angular-basic-persister
- @tanstack/query-example-angular-devtools-panel
- @tanstack/query-example-angular-infinite-query-with-max-pages
- @tanstack/query-example-angular-optimistic-updates
- @tanstack/query-example-angular-pagination
- @tanstack/query-example-angular-query-options-from-a-service
- @tanstack/query-example-angular-router
- @tanstack/query-example-angular-rxjs
- @tanstack/query-example-angular-simple
- @tanstack/query-example-solid-astro
- @tanstack/query-example-solid-basic
- @tanstack/query-example-solid-basic-graphql-request
- @tanstack/query-example-solid-default-query-function
- @tanstack/query-example-solid-simple
- @tanstack/query-example-solid-start-streaming
- @tanstack/query-example-react-algolia
- @tanstack/query-example-react-auto-refetching
- @tanstack/query-example-react-basic
- @tanstack/query-example-react-basic-graphql-request
- @tanstack/query-example-chat
- @tanstack/query-example-react-default-query-function
- @tanstack/query-example-react-devtools-panel
- @tanstack/query-example-eslint-legacy
- @tanstack/query-example-react-infinite-query-with-max-pages
- @tanstack/query-example-react-load-more-infinite-scroll
- @tanstack/query-example-react-nextjs
- @tanstack/query-example-react-nextjs-app-prefetching
- @tanstack/query-example-nextjs-suspense-streaming
- @tanstack/query-example-react-offline
- @tanstack/query-example-react-optimistic-updates-cache
- @tanstack/query-example-react-optimistic-updates-ui
- @tanstack/query-example-react-pagination
- @tanstack/query-example-react-playground
- @tanstack/query-example-react-prefetching
- @tanstack/query-example-react-react-native
- @tanstack/query-example-react-router
- @tanstack/query-example-react-rick-morty
- @tanstack/query-example-react-shadow-dom
- @tanstack/query-example-react-simple
- @tanstack/query-example-react-star-wars
- @tanstack/query-example-react-suspense
- @tanstack/query-example-svelte-auto-refetching
- @tanstack/query-example-svelte-basic
- @tanstack/query-example-svelte-load-more-infinite-scroll
- @tanstack/query-example-svelte-optimistic-updates
- @tanstack/query-example-svelte-playground
- @tanstack/query-example-svelte-simple
- @tanstack/query-example-svelte-ssr
- @tanstack/query-example-svelte-star-wars
- @tanstack/query-example-vue-2.6-basic
- @tanstack/query-example-vue-2.7-basic
- @tanstack/query-example-vue-basic
- @tanstack/query-example-vue-dependent-queries
- @tanstack/query-example-vue-nuxt3
- @tanstack/query-example-vue-persister
- @tanstack/query-example-vue-simple
@tanstack/angular-query-experimental
npm i https://pkg.pr.new/@tanstack/angular-query-experimental@9810
@tanstack/eslint-plugin-query
npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@9810
@tanstack/query-async-storage-persister
npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@9810
@tanstack/query-broadcast-client-experimental
npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@9810
@tanstack/query-core
npm i https://pkg.pr.new/@tanstack/query-core@9810
@tanstack/query-devtools
npm i https://pkg.pr.new/@tanstack/query-devtools@9810
@tanstack/query-persist-client-core
npm i https://pkg.pr.new/@tanstack/query-persist-client-core@9810
@tanstack/query-sync-storage-persister
npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@9810
@tanstack/react-query
npm i https://pkg.pr.new/@tanstack/react-query@9810
@tanstack/react-query-devtools
npm i https://pkg.pr.new/@tanstack/react-query-devtools@9810
@tanstack/react-query-next-experimental
npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@9810
@tanstack/react-query-persist-client
npm i https://pkg.pr.new/@tanstack/react-query-persist-client@9810
@tanstack/solid-query
npm i https://pkg.pr.new/@tanstack/solid-query@9810
@tanstack/solid-query-devtools
npm i https://pkg.pr.new/@tanstack/solid-query-devtools@9810
@tanstack/solid-query-persist-client
npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@9810
@tanstack/svelte-query
npm i https://pkg.pr.new/@tanstack/svelte-query@9810
@tanstack/svelte-query-devtools
npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@9810
@tanstack/svelte-query-persist-client
npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@9810
@tanstack/vue-query
npm i https://pkg.pr.new/@tanstack/vue-query@9810
@tanstack/vue-query-devtools
npm i https://pkg.pr.new/@tanstack/vue-query-devtools@9810
commit: 59a1acc
@lachlancollins could you take a look? This is blocking people from using Tanstack + async Svelte.
This also solves an issue I have where queryFn is called twice in my test after I change the queryOptions. It looks like the effect is triggered when I change the queryOptions.
Some debugging statements in the code (without patch applied):
Component - queryOptions recalculated
createBaseQuery - effect triggered
createBaseQuery - effect triggered
queryFn called
Component - queryOptions recalculated
createBaseQuery - options changed
queryFn called
createBaseQuery - options or observer changed
createBaseQuery - effect triggered
queryFn called
With the patch applied:
Component - queryOptions recalculated
queryFn called
Component - queryOptions recalculated
createBaseQuery - options changed
queryFn called
createBaseQuery - options or observer changed