diff --git a/.changeset/shiny-walls-battle.md b/.changeset/shiny-walls-battle.md new file mode 100644 index 00000000000..97e4ebd18cd --- /dev/null +++ b/.changeset/shiny-walls-battle.md @@ -0,0 +1,5 @@ +--- +'@tanstack/svelte-query': patch +--- + +fix(svelte-query): fix reactive observer subscription lifecycle during restoration diff --git a/packages/svelte-query/src/createBaseQuery.svelte.ts b/packages/svelte-query/src/createBaseQuery.svelte.ts index 03fc6b28db4..a6a456d74e4 100644 --- a/packages/svelte-query/src/createBaseQuery.svelte.ts +++ b/packages/svelte-query/src/createBaseQuery.svelte.ts @@ -1,3 +1,4 @@ +import { untrack } from 'svelte' import { useIsRestoring } from './useIsRestoring.js' import { useQueryClient } from './useQueryClient.js' import { createRawRef } from './containers.svelte.js' @@ -46,6 +47,7 @@ export function createBaseQuery< resolvedOptions, ), ) + watchChanges( () => client, 'pre', @@ -66,19 +68,17 @@ export function createBaseQuery< ? observer.trackResult(result) : result } + const [query, update] = createRawRef( // svelte-ignore state_referenced_locally - intentional, initial value createResult(), ) - $effect(() => { - const unsubscribe = isRestoring.current - ? () => undefined - : observer.subscribe(() => update(createResult())) - observer.updateResult() - return unsubscribe - }) + const refreshResult = () => { + untrack(() => update(createResult())) + } + // Keep observer options updated before DOM flush watchChanges( () => resolvedOptions, 'pre', @@ -86,22 +86,23 @@ export function createBaseQuery< observer.setOptions(resolvedOptions) }, ) - watchChanges( - () => [resolvedOptions, observer], - 'pre', - () => { - // The only reason this is necessary is because of `isRestoring`. - // Because we don't subscribe while restoring, the following can occur: - // - `isRestoring` is true - // - `isRestoring` becomes false - // - `observer.subscribe` and `observer.updateResult` is called in the above effect, - // but the subsequent `fetch` has already completed - // - `result` misses the intermediate restored-but-not-fetched state - // - // this could technically be its own effect but that doesn't seem necessary - update(createResult()) - }, - ) + + // Manage subscription lifecycle reactively to prevent restoration race conditions + $effect(() => { + if (isRestoring.current) { + return + } + + const unsubscribe = observer.subscribe(refreshResult) + + // Surface any state that settled between render and subscription commit without tracking resolvedOptions + refreshResult() + observer.updateResult() + + return () => { + unsubscribe() + } + }) return query -} +} \ No newline at end of file diff --git a/packages/svelte-query/tests/createQuery/IsRestoringDynamic.svelte b/packages/svelte-query/tests/createQuery/IsRestoringDynamic.svelte new file mode 100644 index 00000000000..866e409e002 --- /dev/null +++ b/packages/svelte-query/tests/createQuery/IsRestoringDynamic.svelte @@ -0,0 +1,31 @@ + + +