From db42d36314e0868cbd75ab653762b84e0ec6e516 Mon Sep 17 00:00:00 2001 From: VedAnt-1004 Date: Sun, 20 Sep 2026 20:41:39 +0530 Subject: [PATCH 1/2] fix(svelte-query): sync observer subscription lifecycle during restoration --- .changeset/shiny-walls-battle.md | 5 ++ .../src/createBaseQuery.svelte.ts | 46 +++++++++---------- .../createQuery/IsRestoringDynamic.svelte | 31 +++++++++++++ .../createQuery/createQuery.svelte.test.ts | 41 +++++++++++++++++ 4 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 .changeset/shiny-walls-battle.md create mode 100644 packages/svelte-query/tests/createQuery/IsRestoringDynamic.svelte 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..a9cd769423f 100644 --- a/packages/svelte-query/src/createBaseQuery.svelte.ts +++ b/packages/svelte-query/src/createBaseQuery.svelte.ts @@ -46,6 +46,7 @@ export function createBaseQuery< resolvedOptions, ), ) + watchChanges( () => client, 'pre', @@ -66,19 +67,13 @@ 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 - }) - + // Keep observer options updated before DOM flush watchChanges( () => resolvedOptions, 'pre', @@ -86,22 +81,25 @@ 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 + + // Manage subscription lifecycle reactively to prevent restoration race conditions + $effect(() => { + if (isRestoring.current) { + return + } + + const unsubscribe = observer.subscribe(() => { update(createResult()) - }, - ) + }) + + // Surface any state that settled between render and subscription commit + update(createResult()) + 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 @@ + + +
+
{query.status}
+
{query.fetchStatus}
+
{query.data ?? 'undefined'}
+
\ No newline at end of file diff --git a/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts b/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts index 3864b369076..f939e4dac74 100644 --- a/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts +++ b/packages/svelte-query/tests/createQuery/createQuery.svelte.test.ts @@ -11,6 +11,7 @@ import { import { promiseWithResolvers, withEffectRoot } from '../utils.svelte.js' import Base from './Base.svelte' import Counter from './Counter.svelte' +import IsRestoringDynamic from './IsRestoringDynamic.svelte' import IsRestoring from './IsRestoring.svelte' import Select from './Select.svelte' import TwoQueries from './TwoQueries.svelte' @@ -1650,4 +1651,44 @@ describe('createQuery', () => { expect(rendered.getByTestId('data')).toHaveTextContent('undefined') expect(queryFn).toHaveBeenCalledTimes(0) }) + + it('should subscribe and fetch when isRestoring transitions to false', async () => { + const key = queryKey() + const queryFn = vi.fn().mockImplementation(async () => { + await sleep(10) + return 'restored-data' + }) + + const isRestoringRef = $state({ current: true }) + + const rendered = render(IsRestoringDynamic, { + props: { + queryClient, + queryFn, + queryKey: key, + isRestoringRef, + }, + }) + + await vi.advanceTimersByTimeAsync(0) + + // While restoring, observer should not fetch + expect(rendered.getByTestId('status')).toHaveTextContent('pending') + expect(rendered.getByTestId('fetchStatus')).toHaveTextContent('idle') + expect(queryFn).toHaveBeenCalledTimes(0) + + // Complete restoration + isRestoringRef.current = false + + // Wait for the effect to attach the subscription and start fetching + await vi.advanceTimersByTimeAsync(0) + expect(rendered.getByTestId('fetchStatus')).toHaveTextContent('fetching') + expect(queryFn).toHaveBeenCalledTimes(1) + + // Let the fetch resolve + await vi.advanceTimersByTimeAsync(10) + expect(rendered.getByTestId('status')).toHaveTextContent('success') + expect(rendered.getByTestId('fetchStatus')).toHaveTextContent('idle') + expect(rendered.getByTestId('data')).toHaveTextContent('restored-data') +}) }) From 3c8011832c835b28feb9a1dcf4b9b74c39f67fa6 Mon Sep 17 00:00:00 2001 From: VedAnt-1004 Date: Sun, 20 Sep 2026 21:13:47 +0530 Subject: [PATCH 2/2] fix(svelte-query): untrack createResult inside subscription effect to avoid refetch loops --- packages/svelte-query/src/createBaseQuery.svelte.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/svelte-query/src/createBaseQuery.svelte.ts b/packages/svelte-query/src/createBaseQuery.svelte.ts index a9cd769423f..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' @@ -73,6 +74,10 @@ export function createBaseQuery< createResult(), ) + const refreshResult = () => { + untrack(() => update(createResult())) + } + // Keep observer options updated before DOM flush watchChanges( () => resolvedOptions, @@ -88,12 +93,10 @@ export function createBaseQuery< return } - const unsubscribe = observer.subscribe(() => { - update(createResult()) - }) + const unsubscribe = observer.subscribe(refreshResult) - // Surface any state that settled between render and subscription commit - update(createResult()) + // Surface any state that settled between render and subscription commit without tracking resolvedOptions + refreshResult() observer.updateResult() return () => {