From a5b9a00aba083e387a1d10b7d14bf4a6ed2e324c Mon Sep 17 00:00:00 2001 From: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com> Date: Sat, 30 May 2026 16:32:04 +0530 Subject: [PATCH 1/5] fix(query-broadcast): sync state for added query events --- .../src/__tests__/index.test.ts | 56 +++++++++++++++++-- .../src/index.ts | 4 ++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/packages/query-broadcast-client-experimental/src/__tests__/index.test.ts b/packages/query-broadcast-client-experimental/src/__tests__/index.test.ts index 6e09d8a86e2..f594b6e1f61 100644 --- a/packages/query-broadcast-client-experimental/src/__tests__/index.test.ts +++ b/packages/query-broadcast-client-experimental/src/__tests__/index.test.ts @@ -1,11 +1,11 @@ -import { QueryClient } from '@tanstack/query-core' -import { beforeEach, describe, expect, it } from 'vitest' +import { QueryClient, type QueryCache, type QueryState } from '@tanstack/query-core' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { broadcastQueryClient } from '..' -import type { QueryCache } from '@tanstack/query-core' describe('broadcastQueryClient', () => { let queryClient: QueryClient let queryCache: QueryCache + const broadcastChannel = 'test_channel' beforeEach(() => { queryClient = new QueryClient() @@ -15,7 +15,7 @@ describe('broadcastQueryClient', () => { it('should subscribe to the query cache', () => { broadcastQueryClient({ queryClient, - broadcastChannel: 'test_channel', + broadcastChannel, }) expect(queryCache.hasListeners()).toBe(true) }) @@ -23,9 +23,55 @@ describe('broadcastQueryClient', () => { it('should not have any listeners after cleanup', () => { const unsubscribe = broadcastQueryClient({ queryClient, - broadcastChannel: 'test_channel', + broadcastChannel, }) unsubscribe() expect(queryCache.hasListeners()).toBe(false) }) + + it('should sync initial query state when query is added from another tab', async () => { + const receiverClient = new QueryClient() + const senderClient = new QueryClient() + + const senderUnsubscribe = broadcastQueryClient({ + queryClient: senderClient, + broadcastChannel, + }) + const receiverUnsubscribe = broadcastQueryClient({ + queryClient: receiverClient, + broadcastChannel, + }) + + const seededData = { value: 'seeded' } + const seededState: QueryState = { + data: seededData, + dataUpdateCount: 1, + dataUpdatedAt: Date.now(), + error: null, + errorUpdateCount: 0, + errorUpdatedAt: 0, + fetchFailureCount: 0, + fetchFailureReason: null, + fetchMeta: null, + isInvalidated: false, + status: 'success', + fetchStatus: 'idle', + } + + senderClient.getQueryCache().build( + senderClient, + { queryKey: ['seeded-query'] }, + seededState, + ) + + const findState = () => + receiverClient.getQueryCache().find({ queryKey: ['seeded-query'] })?.state + + await vi.waitFor(() => { + expect(findState()).toMatchObject(seededState) + }) + + senderUnsubscribe() + receiverUnsubscribe() + }) }) diff --git a/packages/query-broadcast-client-experimental/src/index.ts b/packages/query-broadcast-client-experimental/src/index.ts index e102b3c0b01..4d915654873 100644 --- a/packages/query-broadcast-client-experimental/src/index.ts +++ b/packages/query-broadcast-client-experimental/src/index.ts @@ -58,6 +58,7 @@ export function broadcastQueryClient({ type: 'added', queryHash, queryKey, + state, }) } }) @@ -92,6 +93,9 @@ export function broadcastQueryClient({ } } else if (type === 'added') { if (query) { + if (!state) { + return + } query.setState(state) return } From b896d79ed3caa5f45bab6b2198af9649e928e7b0 Mon Sep 17 00:00:00 2001 From: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com> Date: Sat, 30 May 2026 16:41:33 +0530 Subject: [PATCH 2/5] fix(svelte-query): preserve array length updates in createRawRef --- .../svelte-query/src/containers.svelte.ts | 14 +++++++- .../tests/containers.svelte.test.ts | 25 ++++++++++++++ .../createQueries.svelte.test.ts | 34 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/packages/svelte-query/src/containers.svelte.ts b/packages/svelte-query/src/containers.svelte.ts index 60d27c68431..4d2c2e17c57 100644 --- a/packages/svelte-query/src/containers.svelte.ts +++ b/packages/svelte-query/src/containers.svelte.ts @@ -31,6 +31,12 @@ export function createRawRef>( ): [T, (newValue: T) => void] { const refObj = (Array.isArray(init) ? [] : {}) as T const hiddenKeys = new SvelteSet() + const isArrayIndex = (value: PropertyKey): value is `${number}` => + typeof value === 'string' && + value !== 'length' && + Number.isInteger(Number(value)) && + Number(value) >= 0 + const out = new Proxy(refObj, { set(target, prop, value, receiver) { hiddenKeys.delete(prop) @@ -51,6 +57,12 @@ export function createRawRef>( state = v }, }) + + if (Array.isArray(target) && isArrayIndex(prop)) { + const parsed = Number(prop) + target.length = Math.max(target.length, parsed + 1) + } + return true }, has: (target, prop) => { @@ -75,7 +87,7 @@ export function createRawRef>( // If we just deleted it, the reactivity system wouldn't have any idea that the value was gone. target[prop] = undefined hiddenKeys.add(prop) - if (Array.isArray(target)) { + if (Array.isArray(target) && isArrayIndex(prop)) { target.length-- } return true diff --git a/packages/svelte-query/tests/containers.svelte.test.ts b/packages/svelte-query/tests/containers.svelte.test.ts index 3511dbb5b5d..da83c814fc3 100644 --- a/packages/svelte-query/tests/containers.svelte.test.ts +++ b/packages/svelte-query/tests/containers.svelte.test.ts @@ -198,6 +198,31 @@ describe('createRawRef', () => { expect(ref).toEqual([7, 8, 9]) }) + it('should update array length when replacing with longer arrays', () => { + const [ref, update] = createRawRef([]) + + expect(ref.length).toBe(0) + + update([1]) + expect(ref).toEqual([1]) + expect(ref.length).toBe(1) + expect(ref[0]).toBe(1) + + update([2, 3]) + expect(ref).toEqual([2, 3]) + expect(ref.length).toBe(2) + }) + + it('should preserve array length when destructuring', () => { + const [ref, update] = createRawRef([]) + + update([1]) + + const [first] = ref + + expect(first).toBe(1) + }) + it('should behave like a regular object when not using `update`', () => { const [ref] = createRawRef>({ a: 1, b: 2 }) diff --git a/packages/svelte-query/tests/createQueries/createQueries.svelte.test.ts b/packages/svelte-query/tests/createQueries/createQueries.svelte.test.ts index 26af47d1c87..5ccd4b3eea9 100644 --- a/packages/svelte-query/tests/createQueries/createQueries.svelte.test.ts +++ b/packages/svelte-query/tests/createQueries/createQueries.svelte.test.ts @@ -341,4 +341,38 @@ describe('createQueries', () => { expect(queryFn1).toHaveBeenCalledTimes(0) expect(queryFn2).toHaveBeenCalledTimes(0) }) + + it( + 'should activate query when started from an empty array', + withEffectRoot(async () => { + const key = queryKey() + const queryFn = () => sleep(10).then(() => 'ok') + let isActive = $state(false) + + const result = createQueries( + () => ({ + queries: isActive + ? [ + { + queryKey: key, + queryFn, + }, + ] + : [], + }), + () => queryClient, + ) + + expect(result.length).toBe(0) + isActive = true + + await vi.advanceTimersByTimeAsync(0) + + expect(result.length).toBe(1) + expect(result[0]).toMatchObject({ data: undefined }) + + await vi.advanceTimersByTimeAsync(10) + expect(result[0].data).toBe('ok') + }), + ) }) From cc9c94d1e086280823f302b05b1f96730b1119f0 Mon Sep 17 00:00:00 2001 From: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com> Date: Sat, 30 May 2026 16:55:25 +0530 Subject: [PATCH 3/5] fix(svelte-query): keep array length stable on multi-index deletions --- .../svelte-query/src/containers.svelte.ts | 29 +++++++++++++++---- .../tests/containers.svelte.test.ts | 20 +++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/packages/svelte-query/src/containers.svelte.ts b/packages/svelte-query/src/containers.svelte.ts index 4d2c2e17c57..82ff9287020 100644 --- a/packages/svelte-query/src/containers.svelte.ts +++ b/packages/svelte-query/src/containers.svelte.ts @@ -31,11 +31,28 @@ export function createRawRef>( ): [T, (newValue: T) => void] { const refObj = (Array.isArray(init) ? [] : {}) as T const hiddenKeys = new SvelteSet() - const isArrayIndex = (value: PropertyKey): value is `${number}` => - typeof value === 'string' && - value !== 'length' && - Number.isInteger(Number(value)) && - Number(value) >= 0 + const isArrayIndex = (value: PropertyKey): value is `${number}` => { + if (typeof value !== 'string' || value === 'length') { + return false + } + + const index = Number(value) + return Number.isInteger(index) && index >= 0 && String(index) === value + } + + const syncLengthFromVisibleIndexes = (target: Array) => { + const visibleIndexes = Object.keys(target).filter( + (key) => isArrayIndex(key) && !hiddenKeys.has(key), + ) + + if (visibleIndexes.length === 0) { + target.length = 0 + return + } + + const maxIndex = visibleIndexes.reduce((max, key) => Math.max(max, Number(key)), -1) + target.length = maxIndex + 1 + } const out = new Proxy(refObj, { set(target, prop, value, receiver) { @@ -88,7 +105,7 @@ export function createRawRef>( target[prop] = undefined hiddenKeys.add(prop) if (Array.isArray(target) && isArrayIndex(prop)) { - target.length-- + syncLengthFromVisibleIndexes(target) } return true } diff --git a/packages/svelte-query/tests/containers.svelte.test.ts b/packages/svelte-query/tests/containers.svelte.test.ts index da83c814fc3..222150cae3d 100644 --- a/packages/svelte-query/tests/containers.svelte.test.ts +++ b/packages/svelte-query/tests/containers.svelte.test.ts @@ -223,6 +223,26 @@ describe('createRawRef', () => { expect(first).toBe(1) }) + it('should preserve array length when multiple trailing elements are removed', () => { + const [ref, update] = createRawRef([1, 2, 3, 4]) + + update([1]) + + expect(ref).toEqual([1]) + expect(ref.length).toBe(1) + }) + + it('should ignore non-canonical array index keys', () => { + const [ref] = createRawRef([]) + + // @ts-expect-error + ref['1e3'] = 1000 + + expect(ref.length).toBe(0) + // @ts-expect-error + expect(ref['1e3']).toBe(1000) + }) + it('should behave like a regular object when not using `update`', () => { const [ref] = createRawRef>({ a: 1, b: 2 }) From 0183eba6062c507959139a98f850cd581f83790c Mon Sep 17 00:00:00 2001 From: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com> Date: Sun, 24 May 2026 22:48:17 +0530 Subject: [PATCH 4/5] fix(solid-query): track resource result fields --- .../src/__tests__/useQuery.test.tsx | 67 +++++++++++++++++++ packages/solid-query/src/useBaseQuery.ts | 32 +++++++++ 2 files changed, 99 insertions(+) diff --git a/packages/solid-query/src/__tests__/useQuery.test.tsx b/packages/solid-query/src/__tests__/useQuery.test.tsx index 745a9ab1cc2..cec4d3a4a5e 100644 --- a/packages/solid-query/src/__tests__/useQuery.test.tsx +++ b/packages/solid-query/src/__tests__/useQuery.test.tsx @@ -30,6 +30,7 @@ import { QueryClient, QueryClientProvider, keepPreviousData, + queryOptions, useQuery, } from '..' import { Blink, mockOnlineManagerIsOnline, setActTimeout } from './utils' @@ -244,6 +245,72 @@ describe('useQuery', () => { expect(rendered.getByText('test')).toBeInTheDocument() }) + it('should fetch when a curried queryOptions result only reads status fields', async () => { + const key = queryKey() + const queryFn = vi.fn((slug: string) => `test-${slug}`) + const fetchQueryOptions = (slug: string) => + queryOptions({ + queryKey: [key, slug], + queryFn: () => queryFn(slug), + }) + + function Page() { + const options = fetchQueryOptions('slug') + const state = useQuery(() => options) + + return ( + + pending + error + success + + ) + } + + const rendered = render(() => ( + + + + )) + + expect(rendered.getByText('pending')).toBeInTheDocument() + await vi.advanceTimersByTimeAsync(10) + expect(queryFn).toHaveBeenCalledTimes(1) + expect(rendered.getByText('success')).toBeInTheDocument() + }) + + it('should fetch when a curried queryOptions result only reads resource fields', async () => { + const key = queryKey() + const queryFn = vi.fn((slug: string) => `test-${slug}`) + const fetchQueryOptions = (slug: string) => + queryOptions({ + queryKey: [key, slug], + queryFn: () => queryFn(slug), + }) + + function Page() { + const options = fetchQueryOptions('slug') + const state = useQuery(() => options) + + void state.error + void state.failureReason + void state.refetch + void state.promise + + return
mounted
+ } + + const rendered = render(() => ( + + + + )) + + expect(rendered.getByText('mounted')).toBeInTheDocument() + await vi.advanceTimersByTimeAsync(10) + expect(queryFn).toHaveBeenCalledTimes(1) + }) + it('should return the correct states for a successful query', async () => { const key = queryKey() const states: Array> = [] diff --git a/packages/solid-query/src/useBaseQuery.ts b/packages/solid-query/src/useBaseQuery.ts index 773d0719e0c..97b665631e0 100644 --- a/packages/solid-query/src/useBaseQuery.ts +++ b/packages/solid-query/src/useBaseQuery.ts @@ -99,6 +99,33 @@ const hydratableObserverResult = < return obj } +const resourceTrackingProps = new Set([ + 'dataUpdatedAt', + 'error', + 'errorUpdatedAt', + 'failureCount', + 'failureReason', + 'errorUpdateCount', + 'isError', + 'isFetched', + 'isFetchedAfterMount', + 'isFetching', + 'isLoading', + 'isPending', + 'isLoadingError', + 'isInitialLoading', + 'isPaused', + 'isPlaceholderData', + 'isRefetchError', + 'isRefetching', + 'isStale', + 'isSuccess', + 'isEnabled', + 'refetch', + 'promise', + 'status', + 'fetchStatus', +]) // Base Query Function that is used to create the query. export function useBaseQuery< TQueryFnData, @@ -381,6 +408,11 @@ export function useBaseQuery< } return queryResource()?.data } + if (resourceTrackingProps.has(prop)) { + // Solid resources are lazy, so status-only consumers still need to read + // the resource once to start the observer subscription and fetch. + queryResource() + } return Reflect.get(target, prop) }, } From 09577e1f7c1fabbf566dd69c6fcdd187802ee52c Mon Sep 17 00:00:00 2001 From: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com> Date: Sun, 24 May 2026 15:26:23 +0530 Subject: [PATCH 5/5] fix(solid-query): fetch on status-only reads --- .changeset/solid-query-status-resource.md | 5 +++++ packages/solid-query/src/__tests__/useQuery.test.tsx | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .changeset/solid-query-status-resource.md diff --git a/.changeset/solid-query-status-resource.md b/.changeset/solid-query-status-resource.md new file mode 100644 index 00000000000..12a340a386f --- /dev/null +++ b/.changeset/solid-query-status-resource.md @@ -0,0 +1,5 @@ +--- +"@tanstack/solid-query": patch +--- + +Start Solid query resources when status fields are read so curried `queryOptions` fetch on mount without requiring `data` access. diff --git a/packages/solid-query/src/__tests__/useQuery.test.tsx b/packages/solid-query/src/__tests__/useQuery.test.tsx index cec4d3a4a5e..44b0346a403 100644 --- a/packages/solid-query/src/__tests__/useQuery.test.tsx +++ b/packages/solid-query/src/__tests__/useQuery.test.tsx @@ -310,7 +310,6 @@ describe('useQuery', () => { await vi.advanceTimersByTimeAsync(10) expect(queryFn).toHaveBeenCalledTimes(1) }) - it('should return the correct states for a successful query', async () => { const key = queryKey() const states: Array> = []