From 4da78177917c898e89a15ba1d659b6cfaf35c228 Mon Sep 17 00:00:00 2001 From: David Roizenman Date: Thu, 24 Jul 2025 13:20:18 -0700 Subject: [PATCH 1/5] fix(svelte-query): don't wrap observers in derived to avoid state_unsafe_mutation fixes useIsFetching and useIsMutating in svelte 5 adapter --- .../svelte-query/src/createBaseQuery.svelte.ts | 14 +++++++++----- .../svelte-query/src/createMutation.svelte.ts | 17 ++++++++--------- .../svelte-query/src/createQueries.svelte.ts | 10 ++++------ 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/svelte-query/src/createBaseQuery.svelte.ts b/packages/svelte-query/src/createBaseQuery.svelte.ts index 8307f5e40f..b0e5bc881d 100644 --- a/packages/svelte-query/src/createBaseQuery.svelte.ts +++ b/packages/svelte-query/src/createBaseQuery.svelte.ts @@ -39,11 +39,15 @@ export function createBaseQuery< }) /** Creates the observer */ - const observer = $derived( - new Observer( - client, - untrack(() => resolvedOptions), - ), + const observer = new Observer< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey + >( + client, + untrack(() => resolvedOptions), ) function createResult() { diff --git a/packages/svelte-query/src/createMutation.svelte.ts b/packages/svelte-query/src/createMutation.svelte.ts index 87ae521570..bbbdc7ef36 100644 --- a/packages/svelte-query/src/createMutation.svelte.ts +++ b/packages/svelte-query/src/createMutation.svelte.ts @@ -26,16 +26,15 @@ export function createMutation< ): CreateMutationResult { const client = useQueryClient(queryClient?.()) - const observer = $derived( - new MutationObserver( - client, - options(), - ), + const observer = new MutationObserver( + client, + options(), ) - const mutate = $state< - CreateMutateFunction - >((variables, mutateOptions) => { + const mutate = >(( + variables, + mutateOptions, + ) => { observer.mutate(variables, mutateOptions).catch(noop) }) @@ -43,7 +42,7 @@ export function createMutation< observer.setOptions(options()) }) - const result = $state(observer.getCurrentResult()) + const result = observer.getCurrentResult() const unsubscribe = observer.subscribe((val) => { notifyManager.batchCalls(() => { diff --git a/packages/svelte-query/src/createQueries.svelte.ts b/packages/svelte-query/src/createQueries.svelte.ts index e546dc600d..e07c95c5ac 100644 --- a/packages/svelte-query/src/createQueries.svelte.ts +++ b/packages/svelte-query/src/createQueries.svelte.ts @@ -216,12 +216,10 @@ export function createQueries< }), ) - const observer = $derived( - new QueriesObserver( - client, - untrack(() => resolvedQueryOptions), - untrack(() => combine as QueriesObserverOptions), - ), + const observer = new QueriesObserver( + client, + untrack(() => resolvedQueryOptions), + untrack(() => combine as QueriesObserverOptions), ) function createResult() { From de3a48ea8a9b41581ffbee03476b2c82b9b32abd Mon Sep 17 00:00:00 2001 From: David Roizenman Date: Thu, 24 Jul 2025 14:07:12 -0700 Subject: [PATCH 2/5] test(svelte-query): wrap (useIs...) tests in QueryClientProvider to test non colocated query --- .../svelte-query/tests/ProviderWrapper.svelte | 14 ++++++++++ .../tests/createQuery.svelte.test.ts | 14 +++++----- .../tests/useIsFetching/BaseExample.svelte | 27 +++++-------------- .../tests/useIsFetching/FetchStatus.svelte | 6 +++++ .../tests/useIsFetching/Query.svelte | 19 +++++++++++++ .../tests/useIsMutating/BaseExample.svelte | 23 +++++----------- .../tests/useIsMutating/MutatingStatus.svelte | 6 +++++ .../tests/useIsMutating/Query.svelte | 14 ++++++++++ 8 files changed, 79 insertions(+), 44 deletions(-) create mode 100644 packages/svelte-query/tests/ProviderWrapper.svelte create mode 100644 packages/svelte-query/tests/useIsFetching/FetchStatus.svelte create mode 100644 packages/svelte-query/tests/useIsFetching/Query.svelte create mode 100644 packages/svelte-query/tests/useIsMutating/MutatingStatus.svelte create mode 100644 packages/svelte-query/tests/useIsMutating/Query.svelte diff --git a/packages/svelte-query/tests/ProviderWrapper.svelte b/packages/svelte-query/tests/ProviderWrapper.svelte new file mode 100644 index 0000000000..b61d2d99da --- /dev/null +++ b/packages/svelte-query/tests/ProviderWrapper.svelte @@ -0,0 +1,14 @@ + + + + {@render children()} + diff --git a/packages/svelte-query/tests/createQuery.svelte.test.ts b/packages/svelte-query/tests/createQuery.svelte.test.ts index 56dfac5fc9..bf2c478536 100644 --- a/packages/svelte-query/tests/createQuery.svelte.test.ts +++ b/packages/svelte-query/tests/createQuery.svelte.test.ts @@ -225,14 +225,12 @@ describe('createQuery', () => { await withEffectRoot(async () => { const { promise, resolve } = promiseWithResolvers() - const query = $derived( - createQuery( - () => ({ - queryKey: key, - queryFn: () => promise, - }), - () => queryClient, - ), + const query = createQuery( + () => ({ + queryKey: key, + queryFn: () => promise, + }), + () => queryClient, ) expect(query).toEqual( diff --git a/packages/svelte-query/tests/useIsFetching/BaseExample.svelte b/packages/svelte-query/tests/useIsFetching/BaseExample.svelte index 00c8f9c2b8..522955c79b 100644 --- a/packages/svelte-query/tests/useIsFetching/BaseExample.svelte +++ b/packages/svelte-query/tests/useIsFetching/BaseExample.svelte @@ -1,24 +1,11 @@ - + + -
isFetching: {isFetching.current}
-
Data: {query.data ?? 'undefined'}
+ +
diff --git a/packages/svelte-query/tests/useIsFetching/FetchStatus.svelte b/packages/svelte-query/tests/useIsFetching/FetchStatus.svelte new file mode 100644 index 0000000000..5b10705709 --- /dev/null +++ b/packages/svelte-query/tests/useIsFetching/FetchStatus.svelte @@ -0,0 +1,6 @@ + + +
isFetching: {isFetching.current}
diff --git a/packages/svelte-query/tests/useIsFetching/Query.svelte b/packages/svelte-query/tests/useIsFetching/Query.svelte new file mode 100644 index 0000000000..3a2eeb669e --- /dev/null +++ b/packages/svelte-query/tests/useIsFetching/Query.svelte @@ -0,0 +1,19 @@ + + + + +
Data: {query.data ?? 'undefined'}
diff --git a/packages/svelte-query/tests/useIsMutating/BaseExample.svelte b/packages/svelte-query/tests/useIsMutating/BaseExample.svelte index de89cc9867..e2e46622c7 100644 --- a/packages/svelte-query/tests/useIsMutating/BaseExample.svelte +++ b/packages/svelte-query/tests/useIsMutating/BaseExample.svelte @@ -1,20 +1,11 @@ - + + -
isMutating: {isMutating.current}
+ +
diff --git a/packages/svelte-query/tests/useIsMutating/MutatingStatus.svelte b/packages/svelte-query/tests/useIsMutating/MutatingStatus.svelte new file mode 100644 index 0000000000..a747ed8326 --- /dev/null +++ b/packages/svelte-query/tests/useIsMutating/MutatingStatus.svelte @@ -0,0 +1,6 @@ + + +
isMutating: {isMutating.current}
diff --git a/packages/svelte-query/tests/useIsMutating/Query.svelte b/packages/svelte-query/tests/useIsMutating/Query.svelte new file mode 100644 index 0000000000..f9cc2504b0 --- /dev/null +++ b/packages/svelte-query/tests/useIsMutating/Query.svelte @@ -0,0 +1,14 @@ + + + From f919a0f70094a956e7cc2bce4b86b3c3c2e6548d Mon Sep 17 00:00:00 2001 From: David Roizenman Date: Thu, 24 Jul 2025 15:08:42 -0700 Subject: [PATCH 3/5] fix(svelte-query): update observers when passed in query client changes --- .../svelte-query/src/containers.svelte.ts | 4 +- .../src/createBaseQuery.svelte.ts | 66 +++++++++++------ .../svelte-query/src/createMutation.svelte.ts | 73 +++++++++++++------ .../svelte-query/src/createQueries.svelte.ts | 12 +-- packages/svelte-query/src/utils.svelte.ts | 44 +++++++++++ .../tests/createQuery.svelte.test.ts | 27 +++++++ 6 files changed, 172 insertions(+), 54 deletions(-) create mode 100644 packages/svelte-query/src/utils.svelte.ts diff --git a/packages/svelte-query/src/containers.svelte.ts b/packages/svelte-query/src/containers.svelte.ts index 080a9092e8..60d27c6843 100644 --- a/packages/svelte-query/src/containers.svelte.ts +++ b/packages/svelte-query/src/containers.svelte.ts @@ -1,4 +1,4 @@ -import { createSubscriber } from 'svelte/reactivity' +import { SvelteSet, createSubscriber } from 'svelte/reactivity' type VoidFn = () => void type Subscriber = (update: VoidFn) => void | VoidFn @@ -30,7 +30,7 @@ export function createRawRef>( init: T, ): [T, (newValue: T) => void] { const refObj = (Array.isArray(init) ? [] : {}) as T - const hiddenKeys = new Set() + const hiddenKeys = new SvelteSet() const out = new Proxy(refObj, { set(target, prop, value, receiver) { hiddenKeys.delete(prop) diff --git a/packages/svelte-query/src/createBaseQuery.svelte.ts b/packages/svelte-query/src/createBaseQuery.svelte.ts index b0e5bc881d..03fc6b28db 100644 --- a/packages/svelte-query/src/createBaseQuery.svelte.ts +++ b/packages/svelte-query/src/createBaseQuery.svelte.ts @@ -1,7 +1,7 @@ -import { untrack } from 'svelte' import { useIsRestoring } from './useIsRestoring.js' import { useQueryClient } from './useQueryClient.js' import { createRawRef } from './containers.svelte.js' +import { watchChanges } from './utils.svelte.js' import type { QueryClient, QueryKey, QueryObserver } from '@tanstack/query-core' import type { Accessor, @@ -39,15 +39,25 @@ export function createBaseQuery< }) /** Creates the observer */ - const observer = new Observer< - TQueryFnData, - TError, - TData, - TQueryData, - TQueryKey - >( - client, - untrack(() => resolvedOptions), + // svelte-ignore state_referenced_locally - intentional, initial value + let observer = $state( + new Observer( + client, + resolvedOptions, + ), + ) + watchChanges( + () => client, + 'pre', + () => { + observer = new Observer< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey + >(client, resolvedOptions) + }, ) function createResult() { @@ -69,19 +79,29 @@ export function createBaseQuery< return unsubscribe }) - $effect.pre(() => { - observer.setOptions(resolvedOptions) - // 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()) - }) + watchChanges( + () => resolvedOptions, + 'pre', + () => { + 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()) + }, + ) return query } diff --git a/packages/svelte-query/src/createMutation.svelte.ts b/packages/svelte-query/src/createMutation.svelte.ts index bbbdc7ef36..3ff7c06d1d 100644 --- a/packages/svelte-query/src/createMutation.svelte.ts +++ b/packages/svelte-query/src/createMutation.svelte.ts @@ -2,6 +2,7 @@ import { onDestroy } from 'svelte' import { MutationObserver, noop, notifyManager } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient.js' +import { watchChanges } from './utils.svelte.js' import type { Accessor, CreateMutateFunction, @@ -24,13 +25,29 @@ export function createMutation< options: Accessor>, queryClient?: Accessor, ): CreateMutationResult { - const client = useQueryClient(queryClient?.()) + const client = $derived(useQueryClient(queryClient?.())) - const observer = new MutationObserver( - client, - options(), + // svelte-ignore state_referenced_locally - intentional, initial value + let observer = $state( + // svelte-ignore state_referenced_locally - intentional, initial value + new MutationObserver( + client, + options(), + ), ) + watchChanges( + () => client, + 'pre', + () => { + observer = new MutationObserver(client, options()) + }, + ) + + $effect.pre(() => { + observer.setOptions(options()) + }) + const mutate = >(( variables, mutateOptions, @@ -38,33 +55,41 @@ export function createMutation< observer.mutate(variables, mutateOptions).catch(noop) }) - $effect.pre(() => { - observer.setOptions(options()) - }) + let result = $derived(observer.getCurrentResult()) - const result = observer.getCurrentResult() + const subscribe = ( + observer: MutationObserver, + ) => + observer.subscribe((val) => { + notifyManager.batchCalls(() => { + Object.assign(result, val) + })() + }) + let unsubscribe = $state(subscribe(observer)) - const unsubscribe = observer.subscribe((val) => { - notifyManager.batchCalls(() => { - Object.assign(result, val) - })() + $effect.pre(() => { + unsubscribe = subscribe(observer) }) onDestroy(() => { unsubscribe() }) + const resultProxy = $derived( + new Proxy(result, { + get: (_, prop) => { + const r = { + ...result, + mutate, + mutateAsync: result.mutate, + } + if (prop == 'value') return r + // @ts-expect-error + return r[prop] + }, + }), + ) + // @ts-expect-error - return new Proxy(result, { - get: (_, prop) => { - const r = { - ...result, - mutate, - mutateAsync: result.mutate, - } - if (prop == 'value') return r - // @ts-expect-error - return r[prop] - }, - }) + return resultProxy } diff --git a/packages/svelte-query/src/createQueries.svelte.ts b/packages/svelte-query/src/createQueries.svelte.ts index e07c95c5ac..dec5756129 100644 --- a/packages/svelte-query/src/createQueries.svelte.ts +++ b/packages/svelte-query/src/createQueries.svelte.ts @@ -1,5 +1,4 @@ import { QueriesObserver } from '@tanstack/query-core' -import { untrack } from 'svelte' import { useIsRestoring } from './useIsRestoring.js' import { createRawRef } from './containers.svelte.js' import { useQueryClient } from './useQueryClient.js' @@ -216,10 +215,13 @@ export function createQueries< }), ) - const observer = new QueriesObserver( - client, - untrack(() => resolvedQueryOptions), - untrack(() => combine as QueriesObserverOptions), + // can't do same as createMutation, as QueriesObserver has no `setOptions` method + const observer = $derived( + new QueriesObserver( + client, + resolvedQueryOptions, + combine as QueriesObserverOptions, + ), ) function createResult() { diff --git a/packages/svelte-query/src/utils.svelte.ts b/packages/svelte-query/src/utils.svelte.ts new file mode 100644 index 0000000000..9e8073aab7 --- /dev/null +++ b/packages/svelte-query/src/utils.svelte.ts @@ -0,0 +1,44 @@ +import { untrack } from 'svelte' +// modified from the great https://github.com/svecosystem/runed +function runEffect( + flush: 'post' | 'pre', + effect: () => void | VoidFunction, +): void { + switch (flush) { + case 'post': + $effect(effect) + break + case 'pre': + $effect.pre(effect) + break + } +} +type Getter = () => T +export const watchChanges = ( + sources: Getter | Array>, + flush: 'post' | 'pre', + effect: ( + values: T | Array, + previousValues: T | undefined | Array, + ) => void, +) => { + let active = false + let previousValues: T | undefined | Array = Array.isArray( + sources, + ) + ? [] + : undefined + runEffect(flush, () => { + const values = Array.isArray(sources) + ? sources.map((source) => source()) + : sources() + if (!active) { + active = true + previousValues = values + return + } + const cleanup = untrack(() => effect(values, previousValues)) + previousValues = values + return cleanup + }) +} diff --git a/packages/svelte-query/tests/createQuery.svelte.test.ts b/packages/svelte-query/tests/createQuery.svelte.test.ts index bf2c478536..9cec8a17b5 100644 --- a/packages/svelte-query/tests/createQuery.svelte.test.ts +++ b/packages/svelte-query/tests/createQuery.svelte.test.ts @@ -1890,4 +1890,31 @@ describe('createQuery', () => { expect(query.error?.message).toBe('Local Error') }), ) + + it( + 'should support changing provided query client', + withEffectRoot(async () => { + const queryClient1 = new QueryClient() + const queryClient2 = new QueryClient() + + let queryClient = $state(queryClient1) + + const key = ['test'] + + createQuery( + () => ({ + queryKey: key, + queryFn: () => Promise.resolve('prefetched'), + }), + () => queryClient, + ) + + expect(queryClient1.getQueryCache().find({ queryKey: key })).toBeDefined() + + queryClient = queryClient2 + flushSync() + + expect(queryClient2.getQueryCache().find({ queryKey: key })).toBeDefined() + }), + ) }) From ac2afcf685313dadc5b95d50e8eb189a24c47d7e Mon Sep 17 00:00:00 2001 From: David Roizenman Date: Thu, 24 Jul 2025 15:57:26 -0700 Subject: [PATCH 4/5] fix(svelte-query): simplify creatMutation sub/unsub --- .../svelte-query/src/createMutation.svelte.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/svelte-query/src/createMutation.svelte.ts b/packages/svelte-query/src/createMutation.svelte.ts index 3ff7c06d1d..a061ee1143 100644 --- a/packages/svelte-query/src/createMutation.svelte.ts +++ b/packages/svelte-query/src/createMutation.svelte.ts @@ -1,5 +1,3 @@ -import { onDestroy } from 'svelte' - import { MutationObserver, noop, notifyManager } from '@tanstack/query-core' import { useQueryClient } from './useQueryClient.js' import { watchChanges } from './utils.svelte.js' @@ -57,22 +55,13 @@ export function createMutation< let result = $derived(observer.getCurrentResult()) - const subscribe = ( - observer: MutationObserver, - ) => - observer.subscribe((val) => { + $effect.pre(() => { + const unsubscribe = observer.subscribe((val) => { notifyManager.batchCalls(() => { Object.assign(result, val) })() }) - let unsubscribe = $state(subscribe(observer)) - - $effect.pre(() => { - unsubscribe = subscribe(observer) - }) - - onDestroy(() => { - unsubscribe() + return unsubscribe }) const resultProxy = $derived( From 62798d6597ff41a43f326d4819a3835aff042199 Mon Sep 17 00:00:00 2001 From: Lachlan Collins <1667261+lachlancollins@users.noreply.github.com> Date: Sat, 27 Sep 2025 22:29:44 +1000 Subject: [PATCH 5/5] Refactor result handling in createMutation.svelte.ts Replace derived state with direct state and add watchChanges for result updates. --- packages/svelte-query/src/createMutation.svelte.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/svelte-query/src/createMutation.svelte.ts b/packages/svelte-query/src/createMutation.svelte.ts index a061ee1143..51ff74827a 100644 --- a/packages/svelte-query/src/createMutation.svelte.ts +++ b/packages/svelte-query/src/createMutation.svelte.ts @@ -53,7 +53,14 @@ export function createMutation< observer.mutate(variables, mutateOptions).catch(noop) }) - let result = $derived(observer.getCurrentResult()) + let result = $state(observer.getCurrentResult()) + watchChanges( + () => observer, + 'pre', + () => { + result = observer.getCurrentResult() + }, + ) $effect.pre(() => { const unsubscribe = observer.subscribe((val) => {