Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/solid-query-client-resolver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/solid-query': patch
---

Resolve the query client context outside reactive memo callbacks.
26 changes: 24 additions & 2 deletions packages/solid-query/src/QueryClientProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,47 @@ 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
}
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<QueryClient | undefined>,
): Accessor<QueryClient> => {
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
Expand Down
49 changes: 49 additions & 0 deletions packages/solid-query/src/__tests__/QueryClientProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -174,4 +176,51 @@ 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(() => (
<QueryClientProvider client={queryClient}>
<Page />
</QueryClientProvider>
))

createRoot((dispose) => {
const client = createMemo(() => resolveClient())

expect(client()).toBe(queryClient)
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(() => <Page />)).not.toThrow()

expect(() =>
createRoot((dispose) => {
const client = createMemo(() => resolveClient())
client()
dispose()
}),
).toThrow('No QueryClient set, use QueryClientProvider to set one')

consoleMock.mockRestore()
})
})
52 changes: 52 additions & 0 deletions packages/solid-query/src/__tests__/useIsFetching.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,4 +260,56 @@ 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)
const queryCache1 = queryClient1.getQueryCache()
const originalSubscribe1 = queryCache1.subscribe.bind(queryCache1)
const unsubscribe1 = vi.fn()

vi.spyOn(queryCache1, 'subscribe').mockImplementation((listener) => {
const cleanup = originalSubscribe1(listener)

return () => {
unsubscribe1()
cleanup()
}
})

function Page() {
const isFetching = useIsFetching(undefined, client)

return <div>isFetching: {isFetching()}</div>
}

const rendered = render(() => <Page />)

const firstQuery = queryClient1.fetchQuery({
queryKey: key1,
queryFn: () => sleep(20).then(() => 'test1'),
})

expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()

setClient(queryClient2)

expect(unsubscribe1).toHaveBeenCalledTimes(1)
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()
})
})
50 changes: 50 additions & 0 deletions packages/solid-query/src/__tests__/useIsMutating.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,56 @@ 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)
const mutationCache1 = queryClient1.getMutationCache()
const originalSubscribe1 = mutationCache1.subscribe.bind(mutationCache1)
const unsubscribe1 = vi.fn()

vi.spyOn(mutationCache1, 'subscribe').mockImplementation((listener) => {
const cleanup = originalSubscribe1(listener)

return () => {
unsubscribe1()
cleanup()
}
})

function Page() {
const isMutating = useIsMutating(undefined, client)

return <div>mutating: {isMutating()}</div>
}

const rendered = render(() => <Page />)

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(unsubscribe1).toHaveBeenCalledTimes(1)
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
Expand Down
5 changes: 3 additions & 2 deletions packages/solid-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -115,7 +115,8 @@ export function useBaseQuery<
) {
type ResourceData = QueryObserverResult<TData, TError>

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
Expand Down
17 changes: 11 additions & 6 deletions packages/solid-query/src/useIsFetching.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createMemo, createSignal, onCleanup } from 'solid-js'
import { useQueryClient } from './QueryClientProvider'
import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js'
import { useQueryClientResolver } from './QueryClientProvider'
import type { QueryFilters } from '@tanstack/query-core'
import type { QueryClient } from './QueryClient'
import type { Accessor } from 'solid-js'
Expand All @@ -8,16 +8,21 @@ export function useIsFetching(
filters?: Accessor<QueryFilters>,
queryClient?: Accessor<QueryClient>,
): Accessor<number> {
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?.()))

const unsubscribe = queryCache().subscribe(() => {
createEffect(() => {
setFetches(client().isFetching(filters?.()))
})

onCleanup(unsubscribe)
const unsubscribe = queryCache().subscribe(() => {
setFetches(client().isFetching(filters?.()))
})

onCleanup(unsubscribe)
})

return fetches
}
17 changes: 11 additions & 6 deletions packages/solid-query/src/useIsMutating.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createMemo, createSignal, onCleanup } from 'solid-js'
import { useQueryClient } from './QueryClientProvider'
import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js'
import { useQueryClientResolver } from './QueryClientProvider'
import type { MutationFilters } from '@tanstack/query-core'
import type { QueryClient } from './QueryClient'
import type { Accessor } from 'solid-js'
Expand All @@ -8,18 +8,23 @@ export function useIsMutating(
filters?: Accessor<MutationFilters>,
queryClient?: Accessor<QueryClient>,
): Accessor<number> {
const client = createMemo(() => useQueryClient(queryClient?.()))
const resolveClient = useQueryClientResolver(queryClient)
const client = createMemo(() => resolveClient())
const mutationCache = createMemo(() => client().getMutationCache())

const [mutations, setMutations] = createSignal(
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
}
5 changes: 3 additions & 2 deletions packages/solid-query/src/useMutation.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -21,7 +21,8 @@ export function useMutation<
options: UseMutationOptions<TData, TError, TVariables, TOnMutateResult>,
queryClient?: Accessor<QueryClient>,
): UseMutationResult<TData, TError, TVariables, TOnMutateResult> {
const client = createMemo(() => useQueryClient(queryClient?.()))
const resolveClient = useQueryClientResolver(queryClient)
const client = createMemo(() => resolveClient())

const observer = new MutationObserver<
TData,
Expand Down
5 changes: 3 additions & 2 deletions packages/solid-query/src/useMutationState.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -31,7 +31,8 @@ export function useMutationState<TResult = MutationState>(
options: Accessor<MutationStateOptions<TResult>> = () => ({}),
queryClient?: Accessor<QueryClient>,
): Accessor<Array<TResult>> {
const client = createMemo(() => useQueryClient(queryClient?.()))
const resolveClient = useQueryClientResolver(queryClient)
const client = createMemo(() => resolveClient())
const mutationCache = createMemo(() => client().getMutationCache())

const [result, setResult] = createSignal(
Expand Down
5 changes: 3 additions & 2 deletions packages/solid-query/src/useQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -196,7 +196,8 @@ export function useQueries<
}>,
queryClient?: Accessor<QueryClient>,
): TCombinedResult {
const client = createMemo(() => useQueryClient(queryClient?.()))
const resolveClient = useQueryClientResolver(queryClient)
const client = createMemo(() => resolveClient())
const isRestoring = useIsRestoring()

const defaultedQueries = createMemo(() =>
Expand Down
Loading