From ba66d6796f6ac81e4e46f14377aa4858a1f74eda Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sat, 25 Oct 2025 23:33:38 +0200 Subject: [PATCH 1/3] fix: support async Svelte 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 `$effect`s 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. --- .changeset/thin-bugs-change.md | 5 ++++ .../src/createBaseQuery.svelte.ts | 26 ++++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 .changeset/thin-bugs-change.md diff --git a/.changeset/thin-bugs-change.md b/.changeset/thin-bugs-change.md new file mode 100644 index 0000000000..efaca4fccb --- /dev/null +++ b/.changeset/thin-bugs-change.md @@ -0,0 +1,5 @@ +--- +'@tanstack/svelte-query': patch +--- + +fix: support async Svelte diff --git a/packages/svelte-query/src/createBaseQuery.svelte.ts b/packages/svelte-query/src/createBaseQuery.svelte.ts index 03fc6b28db..be829cd597 100644 --- a/packages/svelte-query/src/createBaseQuery.svelte.ts +++ b/packages/svelte-query/src/createBaseQuery.svelte.ts @@ -8,6 +8,7 @@ import type { CreateBaseQueryOptions, CreateBaseQueryResult, } from './types.js' +import { onDestroy } from 'svelte' /** * Base implementation for `createQuery` and `createInfiniteQuery` @@ -71,12 +72,29 @@ export function createBaseQuery< createResult(), ) - $effect(() => { - const unsubscribe = isRestoring.current + // The following is convoluted but necessary: + // Call eagerly so subscription happens on the server and on suspended branches in the client... + let unsubscribe = + isRestoring.current && typeof window !== 'undefined' ? () => undefined : observer.subscribe(() => update(createResult())) - observer.updateResult() - return unsubscribe + // ...but also watch for state changes to resubscribe, and because Svelte right now doesn't + // run onDestroy on components with pending work that are destroyed again before they are resolved... + watchChanges( + () => [isRestoring.current, observer] as const, + 'pre', + () => { + unsubscribe() + unsubscribe = isRestoring.current + ? () => undefined + : observer.subscribe(() => update(createResult())) + observer.updateResult() + return unsubscribe + }, + ) + // ...and finally also cleanup via onDestroy because that one runs on the server whereas $effect.pre does not. + onDestroy(() => { + unsubscribe() }) watchChanges( From 1cacfcb666474e89696e5d6413916a4d30c0bea3 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sat, 25 Oct 2025 23:40:39 +0200 Subject: [PATCH 2/3] lint --- packages/svelte-query/src/createBaseQuery.svelte.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte-query/src/createBaseQuery.svelte.ts b/packages/svelte-query/src/createBaseQuery.svelte.ts index be829cd597..eae6aaebf7 100644 --- a/packages/svelte-query/src/createBaseQuery.svelte.ts +++ b/packages/svelte-query/src/createBaseQuery.svelte.ts @@ -1,3 +1,4 @@ +import { onDestroy } from 'svelte' import { useIsRestoring } from './useIsRestoring.js' import { useQueryClient } from './useQueryClient.js' import { createRawRef } from './containers.svelte.js' @@ -8,7 +9,6 @@ import type { CreateBaseQueryOptions, CreateBaseQueryResult, } from './types.js' -import { onDestroy } from 'svelte' /** * Base implementation for `createQuery` and `createInfiniteQuery` From 59a1acc5697b56e3c73296dd245b3b36b9fff949 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sat, 25 Oct 2025 23:56:27 +0200 Subject: [PATCH 3/3] fix --- packages/svelte-query/src/createBaseQuery.svelte.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/svelte-query/src/createBaseQuery.svelte.ts b/packages/svelte-query/src/createBaseQuery.svelte.ts index eae6aaebf7..401a471814 100644 --- a/packages/svelte-query/src/createBaseQuery.svelte.ts +++ b/packages/svelte-query/src/createBaseQuery.svelte.ts @@ -93,9 +93,13 @@ export function createBaseQuery< }, ) // ...and finally also cleanup via onDestroy because that one runs on the server whereas $effect.pre does not. - onDestroy(() => { - unsubscribe() - }) + // (in a try-catch because it theoretically can be called in a non-component context - that should not happen + // but it would be a breaking change technically to error out here. SSR-safe because this wouldn't be called during SSR if it was not in a component) + try { + onDestroy(() => { + unsubscribe() + }) + } catch (e) {} watchChanges( () => resolvedOptions,