From 9e204dc3adc84735746640e18f96687796dcb46a Mon Sep 17 00:00:00 2001 From: KimHyeongRae0 Date: Sat, 25 Apr 2026 00:30:01 +0900 Subject: [PATCH 1/2] fix(solid-query): resolve query client outside memos --- .changeset/solid-query-client-resolver.md | 5 ++++ .../solid-query/src/QueryClientProvider.tsx | 26 +++++++++++++++++-- .../__tests__/QueryClientProvider.test.tsx | 25 ++++++++++++++++++ packages/solid-query/src/useBaseQuery.ts | 5 ++-- packages/solid-query/src/useIsFetching.ts | 5 ++-- packages/solid-query/src/useIsMutating.ts | 5 ++-- packages/solid-query/src/useMutation.ts | 5 ++-- packages/solid-query/src/useMutationState.ts | 5 ++-- packages/solid-query/src/useQueries.ts | 5 ++-- 9 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 .changeset/solid-query-client-resolver.md diff --git a/.changeset/solid-query-client-resolver.md b/.changeset/solid-query-client-resolver.md new file mode 100644 index 00000000000..ef5ae0eb0f1 --- /dev/null +++ b/.changeset/solid-query-client-resolver.md @@ -0,0 +1,5 @@ +--- +'@tanstack/solid-query': patch +--- + +Resolve the query client context outside reactive memo callbacks. diff --git a/packages/solid-query/src/QueryClientProvider.tsx b/packages/solid-query/src/QueryClientProvider.tsx index 6cde56f22d2..fb461c9eafb 100644 --- a/packages/solid-query/src/QueryClientProvider.tsx +++ b/packages/solid-query/src/QueryClientProvider.tsx @@ -5,12 +5,15 @@ import { useContext, } from 'solid-js' import type { QueryClient } from './QueryClient' -import type { JSX } from 'solid-js' +import type { Accessor, JSX } from 'solid-js' export const QueryClientContext = createContext< (() => QueryClient) | undefined >(undefined) +const queryClientContextError = + 'No QueryClient set, use QueryClientProvider to set one' + export const useQueryClient = (queryClient?: QueryClient) => { if (queryClient) { return queryClient @@ -18,12 +21,31 @@ export const useQueryClient = (queryClient?: QueryClient) => { const client = useContext(QueryClientContext) if (!client) { - throw new Error('No QueryClient set, use QueryClientProvider to set one') + throw new Error(queryClientContextError) } return client() } +export const useQueryClientResolver = ( + queryClient?: Accessor, +): Accessor => { + const contextClient = useContext(QueryClientContext) + + return () => { + const resolvedClient = queryClient?.() + if (resolvedClient) { + return resolvedClient + } + + if (!contextClient) { + throw new Error(queryClientContextError) + } + + return contextClient() + } +} + export type QueryClientProviderProps = { client: QueryClient children?: JSX.Element diff --git a/packages/solid-query/src/__tests__/QueryClientProvider.test.tsx b/packages/solid-query/src/__tests__/QueryClientProvider.test.tsx index e96be03cd24..abe734117a7 100644 --- a/packages/solid-query/src/__tests__/QueryClientProvider.test.tsx +++ b/packages/solid-query/src/__tests__/QueryClientProvider.test.tsx @@ -2,7 +2,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { render } from '@solidjs/testing-library' import { QueryCache } from '@tanstack/query-core' import { queryKey, sleep } from '@tanstack/query-test-utils' +import { createMemo, createRoot } from 'solid-js' import { QueryClient, QueryClientProvider, useQuery, useQueryClient } from '..' +import { useQueryClientResolver } from '../QueryClientProvider' describe('QueryClientProvider', () => { beforeEach(() => { @@ -174,4 +176,27 @@ describe('QueryClientProvider', () => { consoleMock.mockRestore() }) + + it('creates a query client resolver that is safe to call in reactive callbacks', () => { + const queryClient = new QueryClient() + let resolveClient!: () => QueryClient + + function Page() { + resolveClient = useQueryClientResolver() + return null + } + + render(() => ( + + + + )) + + createRoot((dispose) => { + const client = createMemo(() => resolveClient()) + + expect(client()).toBe(queryClient) + dispose() + }) + }) }) diff --git a/packages/solid-query/src/useBaseQuery.ts b/packages/solid-query/src/useBaseQuery.ts index 773d0719e0c..42096176f67 100644 --- a/packages/solid-query/src/useBaseQuery.ts +++ b/packages/solid-query/src/useBaseQuery.ts @@ -12,7 +12,7 @@ import { onCleanup, } from 'solid-js' import { createStore, reconcile, unwrap } from 'solid-js/store' -import { useQueryClient } from './QueryClientProvider' +import { useQueryClientResolver } from './QueryClientProvider' import { useIsRestoring } from './isRestoring' import type { UseBaseQueryOptions } from './types' import type { Accessor, Signal } from 'solid-js' @@ -115,7 +115,8 @@ export function useBaseQuery< ) { type ResourceData = QueryObserverResult - const client = createMemo(() => useQueryClient(queryClient?.())) + const resolveClient = useQueryClientResolver(queryClient) + const client = createMemo(() => resolveClient()) const isRestoring = useIsRestoring() // There are times when we run a query on the server but the resource is never read // This could lead to times when the queryObserver is unsubscribed before the resource has loaded diff --git a/packages/solid-query/src/useIsFetching.ts b/packages/solid-query/src/useIsFetching.ts index e118a5d0762..7733e22ecc2 100644 --- a/packages/solid-query/src/useIsFetching.ts +++ b/packages/solid-query/src/useIsFetching.ts @@ -1,5 +1,5 @@ import { createMemo, createSignal, onCleanup } from 'solid-js' -import { useQueryClient } from './QueryClientProvider' +import { useQueryClientResolver } from './QueryClientProvider' import type { QueryFilters } from '@tanstack/query-core' import type { QueryClient } from './QueryClient' import type { Accessor } from 'solid-js' @@ -8,7 +8,8 @@ export function useIsFetching( filters?: Accessor, queryClient?: Accessor, ): Accessor { - const client = createMemo(() => useQueryClient(queryClient?.())) + const resolveClient = useQueryClientResolver(queryClient) + const client = createMemo(() => resolveClient()) const queryCache = createMemo(() => client().getQueryCache()) const [fetches, setFetches] = createSignal(client().isFetching(filters?.())) diff --git a/packages/solid-query/src/useIsMutating.ts b/packages/solid-query/src/useIsMutating.ts index 57ec870e459..fd2db925578 100644 --- a/packages/solid-query/src/useIsMutating.ts +++ b/packages/solid-query/src/useIsMutating.ts @@ -1,5 +1,5 @@ import { createMemo, createSignal, onCleanup } from 'solid-js' -import { useQueryClient } from './QueryClientProvider' +import { useQueryClientResolver } from './QueryClientProvider' import type { MutationFilters } from '@tanstack/query-core' import type { QueryClient } from './QueryClient' import type { Accessor } from 'solid-js' @@ -8,7 +8,8 @@ export function useIsMutating( filters?: Accessor, queryClient?: Accessor, ): Accessor { - const client = createMemo(() => useQueryClient(queryClient?.())) + const resolveClient = useQueryClientResolver(queryClient) + const client = createMemo(() => resolveClient()) const mutationCache = createMemo(() => client().getMutationCache()) const [mutations, setMutations] = createSignal( diff --git a/packages/solid-query/src/useMutation.ts b/packages/solid-query/src/useMutation.ts index 056766a6433..2a2a8596520 100644 --- a/packages/solid-query/src/useMutation.ts +++ b/packages/solid-query/src/useMutation.ts @@ -1,7 +1,7 @@ import { MutationObserver, noop, shouldThrowError } from '@tanstack/query-core' import { createComputed, createMemo, on, onCleanup } from 'solid-js' import { createStore } from 'solid-js/store' -import { useQueryClient } from './QueryClientProvider' +import { useQueryClientResolver } from './QueryClientProvider' import type { DefaultError } from '@tanstack/query-core' import type { QueryClient } from './QueryClient' import type { @@ -21,7 +21,8 @@ export function useMutation< options: UseMutationOptions, queryClient?: Accessor, ): UseMutationResult { - const client = createMemo(() => useQueryClient(queryClient?.())) + const resolveClient = useQueryClientResolver(queryClient) + const client = createMemo(() => resolveClient()) const observer = new MutationObserver< TData, diff --git a/packages/solid-query/src/useMutationState.ts b/packages/solid-query/src/useMutationState.ts index 2a405a1ae4b..cf5732c35e9 100644 --- a/packages/solid-query/src/useMutationState.ts +++ b/packages/solid-query/src/useMutationState.ts @@ -1,6 +1,6 @@ import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js' import { replaceEqualDeep } from '@tanstack/query-core' -import { useQueryClient } from './QueryClientProvider' +import { useQueryClientResolver } from './QueryClientProvider' import type { Mutation, MutationCache, @@ -31,7 +31,8 @@ export function useMutationState( options: Accessor> = () => ({}), queryClient?: Accessor, ): Accessor> { - const client = createMemo(() => useQueryClient(queryClient?.())) + const resolveClient = useQueryClientResolver(queryClient) + const client = createMemo(() => resolveClient()) const mutationCache = createMemo(() => client().getMutationCache()) const [result, setResult] = createSignal( diff --git a/packages/solid-query/src/useQueries.ts b/packages/solid-query/src/useQueries.ts index 1e5592775db..a0abe68fc1c 100644 --- a/packages/solid-query/src/useQueries.ts +++ b/packages/solid-query/src/useQueries.ts @@ -11,7 +11,7 @@ import { onCleanup, onMount, } from 'solid-js' -import { useQueryClient } from './QueryClientProvider' +import { useQueryClientResolver } from './QueryClientProvider' import { useIsRestoring } from './isRestoring' import type { SolidQueryOptions, UseQueryResult } from './types' import type { Accessor } from 'solid-js' @@ -196,7 +196,8 @@ export function useQueries< }>, queryClient?: Accessor, ): TCombinedResult { - const client = createMemo(() => useQueryClient(queryClient?.())) + const resolveClient = useQueryClientResolver(queryClient) + const client = createMemo(() => resolveClient()) const isRestoring = useIsRestoring() const defaultedQueries = createMemo(() => From 0843b315103acc27fced3fbc8ecfda6b9f1e95a6 Mon Sep 17 00:00:00 2001 From: KimHyeongRae0 <42205606+KimHyeongRae0@users.noreply.github.com> Date: Sun, 3 May 2026 01:42:32 +0900 Subject: [PATCH 2/2] fix(solid-query): resubscribe client counters --- .../__tests__/QueryClientProvider.test.tsx | 24 ++++++++++++ .../src/__tests__/useIsFetching.test.tsx | 39 +++++++++++++++++++ .../src/__tests__/useIsMutating.test.tsx | 37 ++++++++++++++++++ packages/solid-query/src/useIsFetching.ts | 12 ++++-- packages/solid-query/src/useIsMutating.ts | 12 ++++-- 5 files changed, 116 insertions(+), 8 deletions(-) diff --git a/packages/solid-query/src/__tests__/QueryClientProvider.test.tsx b/packages/solid-query/src/__tests__/QueryClientProvider.test.tsx index abe734117a7..f0bb52eca88 100644 --- a/packages/solid-query/src/__tests__/QueryClientProvider.test.tsx +++ b/packages/solid-query/src/__tests__/QueryClientProvider.test.tsx @@ -199,4 +199,28 @@ describe('QueryClientProvider', () => { dispose() }) }) + + it('defers missing provider errors until a resolver is called', () => { + const consoleMock = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined) + let resolveClient!: () => QueryClient + + function Page() { + resolveClient = useQueryClientResolver() + return null + } + + expect(() => render(() => )).not.toThrow() + + expect(() => + createRoot((dispose) => { + const client = createMemo(() => resolveClient()) + client() + dispose() + }), + ).toThrow('No QueryClient set, use QueryClientProvider to set one') + + consoleMock.mockRestore() + }) }) diff --git a/packages/solid-query/src/__tests__/useIsFetching.test.tsx b/packages/solid-query/src/__tests__/useIsFetching.test.tsx index 19edce5e08d..951f407a88d 100644 --- a/packages/solid-query/src/__tests__/useIsFetching.test.tsx +++ b/packages/solid-query/src/__tests__/useIsFetching.test.tsx @@ -260,4 +260,43 @@ describe('useIsFetching', () => { await vi.advanceTimersByTimeAsync(10) expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() }) + + it('should resubscribe when a custom queryClient changes', async () => { + const queryClient1 = new QueryClient() + const queryClient2 = new QueryClient() + const key1 = queryKey() + const key2 = queryKey() + const [client, setClient] = createSignal(queryClient1) + + function Page() { + const isFetching = useIsFetching(undefined, client) + + return
isFetching: {isFetching()}
+ } + + const rendered = render(() => ) + + const firstQuery = queryClient1.fetchQuery({ + queryKey: key1, + queryFn: () => sleep(20).then(() => 'test1'), + }) + + expect(rendered.getByText('isFetching: 1')).toBeInTheDocument() + + setClient(queryClient2) + + expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() + + const secondQuery = queryClient2.fetchQuery({ + queryKey: key2, + queryFn: () => sleep(20).then(() => 'test2'), + }) + + expect(rendered.getByText('isFetching: 1')).toBeInTheDocument() + + await vi.advanceTimersByTimeAsync(20) + await Promise.all([firstQuery, secondQuery]) + + expect(rendered.getByText('isFetching: 0')).toBeInTheDocument() + }) }) diff --git a/packages/solid-query/src/__tests__/useIsMutating.test.tsx b/packages/solid-query/src/__tests__/useIsMutating.test.tsx index 985ce05470a..cbd7a32df97 100644 --- a/packages/solid-query/src/__tests__/useIsMutating.test.tsx +++ b/packages/solid-query/src/__tests__/useIsMutating.test.tsx @@ -207,6 +207,43 @@ describe('useIsMutating', () => { expect(rendered.getByText('mutating: 0')).toBeInTheDocument() }) + it('should resubscribe when a custom queryClient changes', async () => { + const queryClient1 = new QueryClient() + const queryClient2 = new QueryClient() + const [client, setClient] = createSignal(queryClient1) + + function Page() { + const isMutating = useIsMutating(undefined, client) + + return
mutating: {isMutating()}
+ } + + const rendered = render(() => ) + + const firstMutation = queryClient1.getMutationCache().build(queryClient1, { + mutationFn: () => sleep(20).then(() => 'data1'), + }) + const firstMutationPromise = firstMutation.execute(undefined) + + expect(rendered.getByText('mutating: 1')).toBeInTheDocument() + + setClient(queryClient2) + + expect(rendered.getByText('mutating: 0')).toBeInTheDocument() + + const secondMutation = queryClient2.getMutationCache().build(queryClient2, { + mutationFn: () => sleep(20).then(() => 'data2'), + }) + const secondMutationPromise = secondMutation.execute(undefined) + + expect(rendered.getByText('mutating: 1')).toBeInTheDocument() + + await vi.advanceTimersByTimeAsync(20) + await Promise.all([firstMutationPromise, secondMutationPromise]) + + expect(rendered.getByText('mutating: 0')).toBeInTheDocument() + }) + // eslint-disable-next-line vitest/expect-expect it('should not change state if unmounted', async () => { // We have to mock the MutationCache to not unsubscribe diff --git a/packages/solid-query/src/useIsFetching.ts b/packages/solid-query/src/useIsFetching.ts index 7733e22ecc2..96a4f23b004 100644 --- a/packages/solid-query/src/useIsFetching.ts +++ b/packages/solid-query/src/useIsFetching.ts @@ -1,4 +1,4 @@ -import { createMemo, createSignal, onCleanup } from 'solid-js' +import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js' import { useQueryClientResolver } from './QueryClientProvider' import type { QueryFilters } from '@tanstack/query-core' import type { QueryClient } from './QueryClient' @@ -14,11 +14,15 @@ export function useIsFetching( const [fetches, setFetches] = createSignal(client().isFetching(filters?.())) - const unsubscribe = queryCache().subscribe(() => { + createEffect(() => { setFetches(client().isFetching(filters?.())) - }) - onCleanup(unsubscribe) + const unsubscribe = queryCache().subscribe(() => { + setFetches(client().isFetching(filters?.())) + }) + + onCleanup(unsubscribe) + }) return fetches } diff --git a/packages/solid-query/src/useIsMutating.ts b/packages/solid-query/src/useIsMutating.ts index fd2db925578..86e6a8bea50 100644 --- a/packages/solid-query/src/useIsMutating.ts +++ b/packages/solid-query/src/useIsMutating.ts @@ -1,4 +1,4 @@ -import { createMemo, createSignal, onCleanup } from 'solid-js' +import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js' import { useQueryClientResolver } from './QueryClientProvider' import type { MutationFilters } from '@tanstack/query-core' import type { QueryClient } from './QueryClient' @@ -16,11 +16,15 @@ export function useIsMutating( client().isMutating(filters?.()), ) - const unsubscribe = mutationCache().subscribe((_result) => { + createEffect(() => { setMutations(client().isMutating(filters?.())) - }) - onCleanup(unsubscribe) + const unsubscribe = mutationCache().subscribe(() => { + setMutations(client().isMutating(filters?.())) + }) + + onCleanup(unsubscribe) + }) return mutations }