From 7cf4f489d1391762057c3d8af922c0e8bd03e5b1 Mon Sep 17 00:00:00 2001 From: renechoi Date: Tue, 28 Jul 2026 02:04:46 +0900 Subject: [PATCH] fix(svelte-query): track keys added to an empty createRawRef createQueries stayed permanently inactive when it was initialized with an empty queries array. Reads of an absent index have no $state.raw field to subscribe to, and length is a plain array property, so a consumer that observed the result while it was empty registered no reactive dependency and never re-ran once queries were added. Track those reads through a key version that update() bumps whenever the key set changes. --- .changeset/khaki-moons-invite.md | 5 +++ .../svelte-query/src/containers.svelte.ts | 19 ++++++++++ .../createQueries.svelte.test.ts | 35 ++++++++++++++++++- 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 .changeset/khaki-moons-invite.md diff --git a/.changeset/khaki-moons-invite.md b/.changeset/khaki-moons-invite.md new file mode 100644 index 00000000000..1df3fc98afe --- /dev/null +++ b/.changeset/khaki-moons-invite.md @@ -0,0 +1,5 @@ +--- +'@tanstack/svelte-query': patch +--- + +Fix `createQueries` staying inactive when it is initialized with an empty `queries` array. diff --git a/packages/svelte-query/src/containers.svelte.ts b/packages/svelte-query/src/containers.svelte.ts index df602bcb289..337aa568420 100644 --- a/packages/svelte-query/src/containers.svelte.ts +++ b/packages/svelte-query/src/containers.svelte.ts @@ -31,7 +31,22 @@ export function createRawRef>( ): [T, (newValue: T) => void] { const refObj = (Array.isArray(init) ? [] : {}) as T const hiddenKeys = new SvelteSet() + // Absent keys have no `$state.raw` field to subscribe to, and `length` is a + // plain array property, so reads of either are tracked through this instead. + // Without it, anything observed while the ref is empty never re-runs. + let keyVersion = $state.raw(0) + const trackKeys = () => keyVersion const out = new Proxy(refObj, { + get(target, prop, receiver) { + if ( + hiddenKeys.has(prop) || + !(prop in target) || + (Array.isArray(target) && prop === 'length') + ) { + trackKeys() + } + return Reflect.get(target, prop, receiver) + }, set(target, prop, value, receiver) { hiddenKeys.delete(prop) if (prop in target) { @@ -88,6 +103,7 @@ export function createRawRef>( const existingKeys = Object.keys(out) const newKeys = Object.keys(newValue) const keysToRemove = existingKeys.filter((key) => !newKeys.includes(key)) + const keysAdded = newKeys.some((key) => !existingKeys.includes(key)) for (const key of keysToRemove) { // @ts-expect-error delete out[key] @@ -100,6 +116,9 @@ export function createRawRef>( // (See above) out[key] = brand(() => newValue[key]) } + if (keysAdded || keysToRemove.length > 0) { + keyVersion++ + } } // we can't pass `init` directly into the proxy because it'll never set the state fields diff --git a/packages/svelte-query/tests/createQueries/createQueries.svelte.test.ts b/packages/svelte-query/tests/createQueries/createQueries.svelte.test.ts index 3b6620406a6..57e2e925209 100644 --- a/packages/svelte-query/tests/createQueries/createQueries.svelte.test.ts +++ b/packages/svelte-query/tests/createQueries/createQueries.svelte.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { render } from '@testing-library/svelte' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryClient, createQueries } from '../../src/index.js' -import { promiseWithResolvers, withEffectRoot } from '../utils.svelte.js' +import { promiseWithResolvers, ref, withEffectRoot } from '../utils.svelte.js' import IsRestoring from './IsRestoring.svelte' import type { CreateQueryResult } from '../../src/index.js' @@ -108,6 +108,39 @@ describe('createQueries', () => { }), ) + it( + 'should track queries added to an initially empty array', + withEffectRoot(async () => { + const key1 = queryKey() + const queries = ref< + Array<{ queryKey: Array; queryFn: () => Promise }> + >([]) + const results: Array> = [] + + const result = createQueries( + () => ({ queries: queries.value }), + () => queryClient, + ) + + $effect(() => { + results.push(result.map((res) => ({ ...res }))) + }) + + await vi.advanceTimersByTimeAsync(0) + expect(results).toMatchObject([[]]) + + queries.value = [ + { queryKey: key1, queryFn: () => sleep(10).then(() => 'data1') }, + ] + + await vi.advanceTimersByTimeAsync(10) + expect(result[0]?.data).toBe('data1') + expect(results.at(-1)).toMatchObject([ + { status: 'success', data: 'data1' }, + ]) + }), + ) + it( 'should combine queries', withEffectRoot(async () => {