snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
autofix.ci / autofix (push) Failing after 4s
Release / Release (push) Has been skipped
GitHub Actions Security Analysis / Run zizmor (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-16 01:13:08 +05:00
commit 3a013ccdfe
3570 changed files with 490194 additions and 0 deletions

View File

@@ -0,0 +1,225 @@
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(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('sets a specific cache for all queries to use', async () => {
const key = queryKey()
const queryCache = new QueryCache()
const queryClient = new QueryClient({ queryCache })
function Page() {
const query = useQuery(() => ({
queryKey: key,
queryFn: () => sleep(10).then(() => 'test'),
}))
return (
<div>
<h1>{query.data}</h1>
</div>
)
}
const rendered = render(() => (
<QueryClientProvider client={queryClient}>
<Page />
</QueryClientProvider>
))
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('test')).toBeInTheDocument()
expect(queryCache.find({ queryKey: key })?.state.data).toBe('test')
})
it('allows multiple caches to be partitioned', async () => {
const key1 = queryKey()
const key2 = queryKey()
const queryCache1 = new QueryCache()
const queryCache2 = new QueryCache()
const queryClient1 = new QueryClient({ queryCache: queryCache1 })
const queryClient2 = new QueryClient({ queryCache: queryCache2 })
function Page1() {
const query = useQuery(() => ({
queryKey: key1,
queryFn: () => sleep(10).then(() => 'test1'),
}))
return (
<div>
<h1>{query.data}</h1>
</div>
)
}
function Page2() {
const query = useQuery(() => ({
queryKey: key2,
queryFn: () => sleep(10).then(() => 'test2'),
}))
return (
<div>
<h1>{query.data}</h1>
</div>
)
}
const rendered = render(() => (
<>
<QueryClientProvider client={queryClient1}>
<Page1 />
</QueryClientProvider>
<QueryClientProvider client={queryClient2}>
<Page2 />
</QueryClientProvider>
</>
))
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('test1')).toBeInTheDocument()
expect(rendered.getByText('test2')).toBeInTheDocument()
expect(queryCache1.find({ queryKey: key1 })?.state.data).toBe('test1')
expect(queryCache1.find({ queryKey: key2 })).toBeUndefined()
expect(queryCache2.find({ queryKey: key1 })).toBeUndefined()
expect(queryCache2.find({ queryKey: key2 })?.state.data).toBe('test2')
})
it("uses defaultOptions for queries when they don't provide their own config", async () => {
const key = queryKey()
const queryCache = new QueryCache()
const queryClient = new QueryClient({
queryCache,
defaultOptions: {
queries: {
gcTime: Infinity,
},
},
})
function Page() {
const query = useQuery(() => ({
queryKey: key,
queryFn: () => sleep(10).then(() => 'test'),
}))
return (
<div>
<h1>{query.data}</h1>
</div>
)
}
const rendered = render(() => (
<QueryClientProvider client={queryClient}>
<Page />
</QueryClientProvider>
))
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('test')).toBeInTheDocument()
expect(queryCache.find({ queryKey: key })?.options.gcTime).toBe(Infinity)
})
describe('useQueryClient', () => {
it('should throw an error if no query client has been set', () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
function Page() {
useQueryClient()
return null
}
expect(() => render(() => <Page />)).toThrow(
'No QueryClient set, use QueryClientProvider to set one',
)
consoleMock.mockRestore()
})
})
it('should not throw an error if user provides custom query client', () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
function Page() {
const client = new QueryClient()
useQueryClient(client)
return null
}
render(() => <Page />)
expect(consoleMock).not.toHaveBeenCalled()
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()
})
})

View File

@@ -0,0 +1,139 @@
import { describe, expectTypeOf, it } from 'vitest'
import { QueryClient, dataTagSymbol, skipToken } from '@tanstack/query-core'
import { queryKey } from '@tanstack/query-test-utils'
import { useInfiniteQuery } from '../useInfiniteQuery'
import { infiniteQueryOptions } from '../infiniteQueryOptions'
import type { InfiniteData } from '@tanstack/query-core'
import type {
DefinedInitialDataInfiniteOptions,
UndefinedInitialDataInfiniteOptions,
} from '../infiniteQueryOptions'
// Regression test for exported infiniteQueryOptions inference under declaration emit.
// TypeScript should be able to name the return type without expanding the
// internal data tag symbols into the consumer's .d.ts output.
export const exportedInfiniteQueryOptions = infiniteQueryOptions({
queryKey: ['invalid'],
getNextPageParam: () => 1,
initialPageParam: 1,
})
describe('infiniteQueryOptions', () => {
it('should work when passed to infiniteQuery', async () => {
const options = infiniteQueryOptions({
getNextPageParam: () => 10,
queryKey: ['key'],
queryFn: () => ({ wow: true }),
initialPageParam: 0,
})
const data = await new QueryClient().infiniteQuery(options)
expectTypeOf(data).toEqualTypeOf<InfiniteData<{ wow: boolean }, number>>()
})
it('should work when passed to infiniteQuery with select', async () => {
const options = infiniteQueryOptions({
getNextPageParam: () => 10,
queryKey: ['key'],
queryFn: () => ({ wow: true }),
initialPageParam: 0,
select: (data) => data.pages,
})
const data = await new QueryClient().infiniteQuery(options)
expectTypeOf(data).toEqualTypeOf<Array<{ wow: boolean }>>()
})
it('should work when passed to infiniteQuery with enabled: false', async () => {
const options = infiniteQueryOptions({
getNextPageParam: () => 10,
queryKey: ['key'],
queryFn: () => ({ wow: true }),
initialPageParam: 0,
enabled: false,
})
const data = await new QueryClient().infiniteQuery(options)
expectTypeOf(data).toEqualTypeOf<InfiniteData<{ wow: boolean }, number>>()
})
it('should work when passed to infiniteQuery with skipToken', async () => {
const options = infiniteQueryOptions({
getNextPageParam: () => 10,
queryKey: ['key'],
queryFn: skipToken,
initialPageParam: 0,
})
const data = await new QueryClient().infiniteQuery(options)
expectTypeOf(data).toEqualTypeOf<InfiniteData<unknown, number>>()
})
it('should infer defined types', () => {
const options = infiniteQueryOptions({
getNextPageParam: () => 10,
queryKey: queryKey(),
queryFn: () => ({ wow: true }),
initialData: {
pageParams: [undefined],
pages: [{ wow: true }],
},
initialPageParam: 0,
})
expectTypeOf(useInfiniteQuery(() => options).data).toEqualTypeOf<
InfiniteData<{ wow: boolean }, unknown>
>()
expectTypeOf(options).toExtend<
ReturnType<
DefinedInitialDataInfiniteOptions<
{ wow: boolean },
Error,
InfiniteData<{ wow: boolean }, unknown>,
Array<string>,
number | undefined
>
>
>()
expectTypeOf(options.queryKey[dataTagSymbol]).toEqualTypeOf<
InfiniteData<{ wow: boolean }>
>()
})
it('should work without defined types', () => {
const options = infiniteQueryOptions({
getNextPageParam: () => undefined,
queryKey: queryKey(),
queryFn: () => ({ wow: true }),
initialPageParam: 0,
})
expectTypeOf(() => useInfiniteQuery(() => options).data).toEqualTypeOf<
() => InfiniteData<{ wow: boolean }, unknown> | undefined
>()
expectTypeOf(options).toExtend<
ReturnType<
UndefinedInitialDataInfiniteOptions<
{ wow: boolean },
Error,
InfiniteData<{ wow: boolean }, unknown>,
Array<string>,
number
>
>
>()
expectTypeOf(options.queryKey[dataTagSymbol]).toEqualTypeOf<
InfiniteData<{
wow: boolean
}>
>()
})
})

View File

@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest'
import { infiniteQueryOptions } from '../infiniteQueryOptions'
describe('infiniteQueryOptions', () => {
it('should return the object received as a parameter without any modification.', () => {
const object = {
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
getNextPageParam: () => null,
initialPageParam: null,
} as const
expect(infiniteQueryOptions(object)).toBe(object)
})
})

View File

@@ -0,0 +1,218 @@
import { assertType, describe, expectTypeOf, it } from 'vitest'
import { QueryClient } from '@tanstack/query-core'
import { queryKey } from '@tanstack/query-test-utils'
import { useIsMutating, useMutation, useMutationState } from '..'
import { mutationOptions } from '../mutationOptions'
import type {
DefaultError,
MutationFunctionContext,
MutationState,
WithRequired,
} from '@tanstack/query-core'
import type { MutationOptions, UseMutationResult } from '../types'
describe('mutationOptions', () => {
it('should not allow excess properties', () => {
mutationOptions({
mutationFn: () => Promise.resolve(5),
// @ts-expect-error this is a good error, because onMutates does not exist!
onMutates: 1000,
mutationKey: queryKey(),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})
it('should infer types for callbacks', () => {
mutationOptions({
mutationFn: () => Promise.resolve(5),
mutationKey: queryKey(),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})
it('should infer types for onError callback', () => {
mutationOptions({
mutationFn: () => {
throw new Error('fail')
},
mutationKey: queryKey(),
onError: (error) => {
expectTypeOf(error).toEqualTypeOf<DefaultError>()
},
})
})
it('should infer types for variables', () => {
mutationOptions<number, DefaultError, { id: string }>({
mutationFn: (vars) => {
expectTypeOf(vars).toEqualTypeOf<{ id: string }>()
return Promise.resolve(5)
},
mutationKey: queryKey(),
})
})
it('should infer result type correctly', () => {
mutationOptions<number, DefaultError, void, { name: string }>({
mutationFn: () => Promise.resolve(5),
mutationKey: queryKey(),
onMutate: () => {
return { name: 'onMutateResult' }
},
onSuccess: (_data, _variables, onMutateResult) => {
expectTypeOf(onMutateResult).toEqualTypeOf<{ name: string }>()
},
})
})
it('should infer context type correctly', () => {
mutationOptions<number>({
mutationFn: (_variables, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
return Promise.resolve(5)
},
mutationKey: queryKey(),
onMutate: (_variables, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onSuccess: (_data, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onError: (_error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
})
})
it('should error if mutationFn return type mismatches TData', () => {
assertType(
mutationOptions<number>({
// @ts-expect-error this is a good error, because return type is string, not number
mutationFn: async () => Promise.resolve('wrong return'),
}),
)
})
it('should allow mutationKey to be omitted', () => {
return mutationOptions({
mutationFn: () => Promise.resolve(123),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})
it('should infer all types when not explicitly provided', () => {
expectTypeOf(
mutationOptions({
mutationFn: (id: string) => Promise.resolve(id.length),
mutationKey: queryKey(),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
}),
).toEqualTypeOf<
WithRequired<MutationOptions<number, DefaultError, string>, 'mutationKey'>
>()
expectTypeOf(
mutationOptions({
mutationFn: (id: string) => Promise.resolve(id.length),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
}),
).toEqualTypeOf<
Omit<MutationOptions<number, DefaultError, string>, 'mutationKey'>
>()
})
it('should infer types when used with useMutation', () => {
const mutation = useMutation(() =>
mutationOptions({
mutationKey: queryKey(),
mutationFn: () => Promise.resolve('data'),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<string>()
},
}),
)
expectTypeOf(mutation).toEqualTypeOf<
UseMutationResult<string, DefaultError, void, unknown>
>()
useMutation(() =>
// should allow when used with useMutation without mutationKey
mutationOptions({
mutationFn: () => Promise.resolve('data'),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<string>()
},
}),
)
})
it('should infer types when used with useIsMutating', () => {
const isMutating = useIsMutating(() =>
mutationOptions({
mutationKey: queryKey(),
mutationFn: () => Promise.resolve(5),
}),
)
expectTypeOf(isMutating).toEqualTypeOf<() => number>()
useIsMutating(
// @ts-expect-error filters should have mutationKey
() =>
mutationOptions({
mutationFn: () => Promise.resolve(5),
}),
)
})
it('should infer types when used with queryClient.isMutating', () => {
const queryClient = new QueryClient()
const isMutating = queryClient.isMutating(
mutationOptions({
mutationKey: queryKey(),
mutationFn: () => Promise.resolve(5),
}),
)
expectTypeOf(isMutating).toEqualTypeOf<number>()
queryClient.isMutating(
// @ts-expect-error filters should have mutationKey
mutationOptions({
mutationFn: () => Promise.resolve(5),
}),
)
})
it('should infer types when used with useMutationState', () => {
const mutationState = useMutationState(() => ({
filters: mutationOptions({
mutationKey: queryKey(),
mutationFn: () => Promise.resolve(5),
}),
}))
expectTypeOf(mutationState).toEqualTypeOf<
() => Array<MutationState<unknown, Error, unknown, unknown>>
>()
useMutationState(
// @ts-expect-error filters should have mutationKey
() => ({
filters: mutationOptions({
mutationFn: () => Promise.resolve(5),
}),
}),
)
})
})

View File

@@ -0,0 +1,511 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent } from '@solidjs/testing-library'
import { createEffect, createRenderEffect } from 'solid-js'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { QueryClient, useIsMutating, useMutation, useMutationState } from '..'
import { mutationOptions } from '../mutationOptions'
import { renderWithClient } from './utils'
import type { MutationState } from '@tanstack/query-core'
describe('mutationOptions', () => {
let queryClient: QueryClient
beforeEach(() => {
vi.useFakeTimers()
queryClient = new QueryClient()
})
afterEach(() => {
queryClient.clear()
vi.useRealTimers()
})
it('should return the object received as a parameter without any modification (with mutationKey in mutationOptions)', () => {
const object = {
mutationKey: ['key'],
mutationFn: () => sleep(10).then(() => 5),
} as const
expect(mutationOptions(object)).toBe(object)
})
it('should return the object received as a parameter without any modification (without mutationKey in mutationOptions)', () => {
const object = {
mutationFn: () => sleep(10).then(() => 5),
} as const
expect(mutationOptions(object)).toBe(object)
})
it('should return the number of fetching mutations when used with useIsMutating (with mutationKey in mutationOptions)', async () => {
const isMutatingArray: Array<number> = []
const key = queryKey()
const mutationOpts = mutationOptions({
mutationKey: key,
mutationFn: () => sleep(50).then(() => 'data'),
})
function Mutation() {
const isMutating = useIsMutating()
const { mutate } = useMutation(() => mutationOpts)
createRenderEffect(() => {
isMutatingArray.push(isMutating())
})
return (
<div>
<button onClick={() => mutate()}>mutate</button>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Mutation />)
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
expect(isMutatingArray[0]).toEqual(0)
await vi.advanceTimersByTimeAsync(0)
expect(isMutatingArray[1]).toEqual(1)
await vi.advanceTimersByTimeAsync(50)
expect(isMutatingArray[2]).toEqual(0)
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
})
it('should return the number of fetching mutations when used with useIsMutating (without mutationKey in mutationOptions)', async () => {
const isMutatingArray: Array<number> = []
const mutationOpts = mutationOptions({
mutationFn: () => sleep(50).then(() => 'data'),
})
function Mutation() {
const isMutating = useIsMutating()
const { mutate } = useMutation(() => mutationOpts)
createRenderEffect(() => {
isMutatingArray.push(isMutating())
})
return (
<div>
<button onClick={() => mutate()}>mutate</button>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Mutation />)
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
expect(isMutatingArray[0]).toEqual(0)
await vi.advanceTimersByTimeAsync(0)
expect(isMutatingArray[1]).toEqual(1)
await vi.advanceTimersByTimeAsync(50)
expect(isMutatingArray[2]).toEqual(0)
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
})
it('should return the number of fetching mutations when used with useIsMutating', async () => {
const isMutatingArray: Array<number> = []
const key = queryKey()
const mutationOpts1 = mutationOptions({
mutationKey: key,
mutationFn: () => sleep(50).then(() => 'data1'),
})
const mutationOpts2 = mutationOptions({
mutationFn: () => sleep(50).then(() => 'data2'),
})
function Mutation() {
const isMutating = useIsMutating()
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
createRenderEffect(() => {
isMutatingArray.push(isMutating())
})
return (
<div>
<button onClick={() => mutate1()}>mutate1</button>
<button onClick={() => mutate2()}>mutate2</button>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Mutation />)
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
expect(isMutatingArray[0]).toEqual(0)
await vi.advanceTimersByTimeAsync(0)
expect(isMutatingArray[2]).toEqual(2)
await vi.advanceTimersByTimeAsync(50)
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
})
it('should return the number of fetching mutations when used with useIsMutating (filter mutationOpts1.mutationKey)', async () => {
const isMutatingArray: Array<number> = []
const key = queryKey()
const mutationOpts1 = mutationOptions({
mutationKey: key,
mutationFn: () => sleep(50).then(() => 'data1'),
})
const mutationOpts2 = mutationOptions({
mutationFn: () => sleep(50).then(() => 'data2'),
})
function Mutation() {
const isMutating = useIsMutating(() => ({
mutationKey: mutationOpts1.mutationKey,
}))
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
createRenderEffect(() => {
isMutatingArray.push(isMutating())
})
return (
<div>
<button onClick={() => mutate1()}>mutate1</button>
<button onClick={() => mutate2()}>mutate2</button>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Mutation />)
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
expect(isMutatingArray[0]).toEqual(0)
await vi.advanceTimersByTimeAsync(0)
expect(isMutatingArray[1]).toEqual(1)
await vi.advanceTimersByTimeAsync(50)
expect(isMutatingArray[2]).toEqual(0)
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
})
it('should return the number of fetching mutations when used with queryClient.isMutating (with mutationKey in mutationOptions)', async () => {
const isMutatingArray: Array<number> = []
const key = queryKey()
const mutationOpts = mutationOptions({
mutationKey: key,
mutationFn: () => sleep(500).then(() => 'data'),
})
function Mutation() {
const { mutate } = useMutation(() => mutationOpts)
return (
<div>
<button onClick={() => mutate()}>mutate</button>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Mutation />)
const mutationCache = queryClient.getMutationCache()
const unsubscribe = mutationCache.subscribe(() => {
isMutatingArray.push(queryClient.isMutating(mutationOpts))
})
isMutatingArray.push(queryClient.isMutating(mutationOpts))
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
expect(isMutatingArray[0]).toEqual(0)
await vi.advanceTimersByTimeAsync(0)
// Use Math.max because subscribe callback count is implementation-dependent
expect(Math.max(...isMutatingArray)).toEqual(1)
await vi.advanceTimersByTimeAsync(500)
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
unsubscribe()
})
it('should return the number of fetching mutations when used with queryClient.isMutating (without mutationKey in mutationOptions)', async () => {
const isMutatingArray: Array<number> = []
const mutationOpts = mutationOptions({
mutationFn: () => sleep(500).then(() => 'data'),
})
function Mutation() {
const { mutate } = useMutation(() => mutationOpts)
return (
<div>
<button onClick={() => mutate()}>mutate</button>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Mutation />)
const mutationCache = queryClient.getMutationCache()
const unsubscribe = mutationCache.subscribe(() => {
isMutatingArray.push(queryClient.isMutating())
})
isMutatingArray.push(queryClient.isMutating())
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
expect(isMutatingArray[0]).toEqual(0)
await vi.advanceTimersByTimeAsync(0)
// Use Math.max because subscribe callback count is implementation-dependent
expect(Math.max(...isMutatingArray)).toEqual(1)
await vi.advanceTimersByTimeAsync(500)
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
unsubscribe()
})
it('should return the number of fetching mutations when used with queryClient.isMutating', async () => {
const isMutatingArray: Array<number> = []
const key = queryKey()
const mutationOpts1 = mutationOptions({
mutationKey: key,
mutationFn: () => sleep(500).then(() => 'data1'),
})
const mutationOpts2 = mutationOptions({
mutationFn: () => sleep(500).then(() => 'data2'),
})
function Mutation() {
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
return (
<div>
<button onClick={() => mutate1()}>mutate1</button>
<button onClick={() => mutate2()}>mutate2</button>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Mutation />)
const mutationCache = queryClient.getMutationCache()
const unsubscribe = mutationCache.subscribe(() => {
isMutatingArray.push(queryClient.isMutating())
})
isMutatingArray.push(queryClient.isMutating())
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
expect(isMutatingArray[0]).toEqual(0)
await vi.advanceTimersByTimeAsync(0)
// Use Math.max because subscribe callback count is implementation-dependent
expect(Math.max(...isMutatingArray)).toEqual(2)
await vi.advanceTimersByTimeAsync(500)
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
unsubscribe()
})
it('should return the number of fetching mutations when used with queryClient.isMutating (filter mutationOpt1.mutationKey)', async () => {
const isMutatingArray: Array<number> = []
const key = queryKey()
const mutationOpts1 = mutationOptions({
mutationKey: key,
mutationFn: () => sleep(500).then(() => 'data1'),
})
const mutationOpts2 = mutationOptions({
mutationFn: () => sleep(500).then(() => 'data2'),
})
function Mutation() {
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
return (
<div>
<button onClick={() => mutate1()}>mutate1</button>
<button onClick={() => mutate2()}>mutate2</button>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Mutation />)
const mutationCache = queryClient.getMutationCache()
const unsubscribe = mutationCache.subscribe(() => {
isMutatingArray.push(
queryClient.isMutating({
mutationKey: mutationOpts1.mutationKey,
}),
)
})
isMutatingArray.push(
queryClient.isMutating({
mutationKey: mutationOpts1.mutationKey,
}),
)
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
expect(isMutatingArray[0]).toEqual(0)
await vi.advanceTimersByTimeAsync(0)
// Use Math.max because subscribe callback count is implementation-dependent
expect(Math.max(...isMutatingArray)).toEqual(1)
await vi.advanceTimersByTimeAsync(500)
expect(isMutatingArray[isMutatingArray.length - 1]).toEqual(0)
unsubscribe()
})
it('should return the number of fetching mutations when used with useMutationState (with mutationKey in mutationOptions)', async () => {
const mutationStateArray: Array<Array<MutationState>> = []
const key = queryKey()
const mutationOpts = mutationOptions({
mutationKey: key,
mutationFn: () => sleep(10).then(() => 'data'),
})
function Mutation() {
const { mutate } = useMutation(() => mutationOpts)
const states = useMutationState(() => ({
filters: { mutationKey: mutationOpts.mutationKey, status: 'success' },
}))
createEffect(() => {
mutationStateArray.push(states())
})
return (
<div>
<button onClick={() => mutate()}>mutate</button>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Mutation />)
expect(mutationStateArray[0]).toEqual([])
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
const lastSnapshot = mutationStateArray[mutationStateArray.length - 1]!
expect(lastSnapshot.length).toEqual(1)
expect(lastSnapshot[0]?.data).toEqual('data')
})
it('should return the number of fetching mutations when used with useMutationState (without mutationKey in mutationOptions)', async () => {
const mutationStateArray: Array<Array<MutationState>> = []
const mutationOpts = mutationOptions({
mutationFn: () => sleep(10).then(() => 'data'),
})
function Mutation() {
const { mutate } = useMutation(() => mutationOpts)
const states = useMutationState(() => ({
filters: { status: 'success' },
}))
createEffect(() => {
mutationStateArray.push(states())
})
return (
<div>
<button onClick={() => mutate()}>mutate</button>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Mutation />)
expect(mutationStateArray[0]).toEqual([])
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(10)
const lastSnapshot = mutationStateArray[mutationStateArray.length - 1]!
expect(lastSnapshot.length).toEqual(1)
expect(lastSnapshot[0]?.data).toEqual('data')
})
it('should return the number of fetching mutations when used with useMutationState', async () => {
const mutationStateArray: Array<Array<MutationState>> = []
const key = queryKey()
const mutationOpts1 = mutationOptions({
mutationKey: key,
mutationFn: () => sleep(10).then(() => 'data1'),
})
const mutationOpts2 = mutationOptions({
mutationFn: () => sleep(10).then(() => 'data2'),
})
function Mutation() {
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
const states = useMutationState(() => ({
filters: { status: 'success' },
}))
createEffect(() => {
mutationStateArray.push(states())
})
return (
<div>
<button onClick={() => mutate1()}>mutate1</button>
<button onClick={() => mutate2()}>mutate2</button>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Mutation />)
expect(mutationStateArray[0]).toEqual([])
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
await vi.advanceTimersByTimeAsync(10)
const lastSnapshot = mutationStateArray[mutationStateArray.length - 1]!
expect(lastSnapshot.length).toEqual(2)
expect(lastSnapshot[0]?.data).toEqual('data1')
expect(lastSnapshot[1]?.data).toEqual('data2')
})
it('should return the number of fetching mutations when used with useMutationState (filter mutationOpt1.mutationKey)', async () => {
const mutationStateArray: Array<Array<MutationState>> = []
const key = queryKey()
const mutationOpts1 = mutationOptions({
mutationKey: key,
mutationFn: () => sleep(10).then(() => 'data1'),
})
const mutationOpts2 = mutationOptions({
mutationFn: () => sleep(10).then(() => 'data2'),
})
function Mutation() {
const { mutate: mutate1 } = useMutation(() => mutationOpts1)
const { mutate: mutate2 } = useMutation(() => mutationOpts2)
const states = useMutationState(() => ({
filters: { mutationKey: mutationOpts1.mutationKey, status: 'success' },
}))
createEffect(() => {
mutationStateArray.push(states())
})
return (
<div>
<button onClick={() => mutate1()}>mutate1</button>
<button onClick={() => mutate2()}>mutate2</button>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Mutation />)
expect(mutationStateArray[0]).toEqual([])
fireEvent.click(rendered.getByRole('button', { name: /mutate1/i }))
fireEvent.click(rendered.getByRole('button', { name: /mutate2/i }))
await vi.advanceTimersByTimeAsync(10)
const lastSnapshot = mutationStateArray[mutationStateArray.length - 1]!
expect(lastSnapshot.length).toEqual(1)
expect(lastSnapshot[0]?.data).toEqual('data1')
})
})

View File

@@ -0,0 +1,209 @@
import { assertType, describe, expectTypeOf, it } from 'vitest'
import {
QueryClient,
dataTagErrorSymbol,
dataTagSymbol,
skipToken,
} from '@tanstack/query-core'
import { queryKey } from '@tanstack/query-test-utils'
import { useQuery } from '../useQuery'
import { queryOptions } from '../queryOptions'
// Regression test for exported queryOptions inference under declaration emit.
// TypeScript should be able to name the return type without expanding the
// internal data tag symbols into the consumer's .d.ts output.
export const exportedQueryOptions = queryOptions({
queryKey: ['invalid'],
})
describe('queryOptions', () => {
it('should not allow excess properties', () => {
assertType(
queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(5),
// @ts-expect-error this is a good error, because stallTime does not exist!
stallTime: 1000,
}),
)
})
it('should infer types for callbacks', () => {
queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(5),
staleTime: 1000,
select: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})
it('should work when passed to useQuery', () => {
const options = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(5),
})
const { data } = useQuery(() => options)
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('should work when passed to query', async () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
})
const data = await new QueryClient().query(options)
expectTypeOf(data).toEqualTypeOf<number>()
})
it('should work when passed to query with select', async () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
select: (data) => data.toString(),
})
const data = await new QueryClient().query(options)
expectTypeOf(data).toEqualTypeOf<string>()
})
it('should work when passed to query with enabled: false', async () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
enabled: false,
})
const data = await new QueryClient().query(options)
expectTypeOf(data).toEqualTypeOf<number>()
})
it('should work when passed to query with skipToken', async () => {
const options = queryOptions({
queryKey: ['key'],
queryFn: skipToken,
})
const data = await new QueryClient().query(options)
expectTypeOf(data).toEqualTypeOf<unknown>()
})
it('should work when passed to fetchQuery', async () => {
const options = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(5),
})
// grandfathered direct test
// eslint-disable-next-line no-restricted-syntax
const data = await new QueryClient().fetchQuery(options)
expectTypeOf(data).toEqualTypeOf<number>()
})
it('should tag the queryKey with the result type of the QueryFn', () => {
const { queryKey: tagged } = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(5),
})
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
})
it('should tag the queryKey even if no promise is returned', () => {
const { queryKey: tagged } = queryOptions({
queryKey: queryKey(),
queryFn: () => 5,
})
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
})
it('should tag the queryKey with unknown if there is no queryFn', () => {
const { queryKey: tagged } = queryOptions({
queryKey: queryKey(),
})
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<unknown>()
})
it('should tag the queryKey with the result type of the QueryFn if select is used', () => {
const { queryKey: tagged } = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(5),
select: (data) => data.toString(),
})
expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<number>()
})
it('should tag the queryKey with the default error type', () => {
const { queryKey: tagged } = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
})
expectTypeOf(tagged[dataTagErrorSymbol]).toEqualTypeOf<Error>()
})
it('should return the proper type when passed to getQueryData', () => {
const { queryKey: tagged } = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(5),
})
const queryClient = new QueryClient()
const data = queryClient.getQueryData(tagged)
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('should return the proper type when passed to getQueryState', () => {
const { queryKey: tagged } = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(5),
})
const queryClient = new QueryClient()
const state = queryClient.getQueryState(tagged)
expectTypeOf(state?.data).toEqualTypeOf<number | undefined>()
})
it('should properly type updaterFn when passed to setQueryData', () => {
const { queryKey: tagged } = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(5),
})
const queryClient = new QueryClient()
const data = queryClient.setQueryData(tagged, (prev) => {
expectTypeOf(prev).toEqualTypeOf<number | undefined>()
return prev
})
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('should properly type value when passed to setQueryData', () => {
const { queryKey: tagged } = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(5),
})
const queryClient = new QueryClient()
// @ts-expect-error value should be a number
queryClient.setQueryData(tagged, '5')
// @ts-expect-error value should be a number
queryClient.setQueryData(tagged, () => '5')
const data = queryClient.setQueryData(tagged, 5)
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('should infer even if there is a conditional skipToken', () => {
const options = queryOptions({
queryKey: queryKey(),
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
})
const queryClient = new QueryClient()
const data = queryClient.getQueryData(options.queryKey)
expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
it('should infer to unknown if we disable a query with just a skipToken', () => {
const options = queryOptions({
queryKey: queryKey(),
queryFn: skipToken,
})
const queryClient = new QueryClient()
const data = queryClient.getQueryData(options.queryKey)
expectTypeOf(data).toEqualTypeOf<unknown>()
})
})

View File

@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest'
import { queryOptions } from '../queryOptions'
describe('queryOptions', () => {
it('should return the object received as a parameter without any modification.', () => {
const object = {
queryKey: ['key'],
queryFn: () => Promise.resolve(5),
} as const
expect(queryOptions(object)).toBe(object)
})
})

View File

@@ -0,0 +1,898 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent } from '@solidjs/testing-library'
import {
ErrorBoundary,
Show,
Suspense,
createRenderEffect,
createSignal,
on,
} from 'solid-js'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { QueryCache, QueryClient, useInfiniteQuery, useQuery } from '..'
import { renderWithClient } from './utils'
import type { InfiniteData, UseInfiniteQueryResult, UseQueryResult } from '..'
describe("useQuery's in Suspense mode", () => {
let queryCache: QueryCache
let queryClient: QueryClient
beforeEach(() => {
vi.useFakeTimers()
queryCache = new QueryCache()
queryClient = new QueryClient({ queryCache })
})
afterEach(() => {
queryClient.clear()
vi.useRealTimers()
})
it('should render the correct amount of times in Suspense mode', async () => {
const key = queryKey()
const states: Array<UseQueryResult<number>> = []
let count = 0
let renders = 0
function Page() {
const [stateKey, setStateKey] = createSignal(key)
const state = useQuery(() => ({
queryKey: stateKey(),
queryFn: () => sleep(10).then(() => ++count),
}))
createRenderEffect(() => {
states.push({ ...state })
})
createRenderEffect(
on([() => ({ ...state }), () => key], () => {
renders++
}),
)
return (
<div>
<button aria-label="toggle" onClick={() => setStateKey(queryKey())} />
data: {String(state.data)}
</div>
)
}
const rendered = renderWithClient(queryClient, () => (
<Suspense fallback="loading">
<Page />
</Suspense>
))
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('data: 1')).toBeInTheDocument()
fireEvent.click(rendered.getByLabelText('toggle'))
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('data: 2')).toBeInTheDocument()
expect(renders).toBe(4)
expect(states.length).toBe(4)
expect(states[1]).toMatchObject({ data: 1, status: 'success' })
expect(states[3]).toMatchObject({ data: 2, status: 'success' })
})
it('should return the correct states for a successful infinite query', async () => {
const key = queryKey()
const states: Array<UseInfiniteQueryResult<InfiniteData<number>>> = []
function Page() {
const [multiplier, setMultiplier] = createSignal(1)
const state = useInfiniteQuery(() => ({
queryKey: [`${key}_${multiplier()}`],
queryFn: ({ pageParam }) =>
sleep(10).then(() => pageParam * multiplier()),
initialPageParam: 1,
suspense: true,
getNextPageParam: (lastPage) => lastPage + 1,
}))
createRenderEffect(() => {
states.push({ ...state })
})
return (
<div>
<button onClick={() => setMultiplier(2)}>next</button>
data: {state.data?.pages.join(',')}
</div>
)
}
const rendered = renderWithClient(queryClient, () => (
<Suspense fallback="loading">
<Page />
</Suspense>
))
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('data: 1')).toBeInTheDocument()
// eslint-disable-next-line cspell/spellchecker
// TODO(lukemurray): in react this is 1 in solid this is 2 because suspense
// occurs on read.
expect(states.length).toBe(2)
expect(states[1]).toMatchObject({
data: { pages: [1], pageParams: [1] },
status: 'success',
})
fireEvent.click(rendered.getByText('next'))
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('data: 2')).toBeInTheDocument()
// eslint-disable-next-line cspell/spellchecker
// TODO(lukemurray): in react this is 2 and in solid it is 4
expect(states.length).toBe(4)
expect(states[3]).toMatchObject({
data: { pages: [2], pageParams: [1] },
status: 'success',
})
})
it('should not call the queryFn twice when used in Suspense mode', async () => {
const key = queryKey()
const queryFn = vi.fn(() => sleep(10).then(() => 'data'))
function Page() {
useQuery(() => ({
queryKey: [key],
queryFn,
suspense: true,
}))
return <>rendered</>
}
const rendered = renderWithClient(queryClient, () => (
<Suspense fallback="loading">
<Page />
</Suspense>
))
expect(rendered.getByText('rendered')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(20)
expect(queryFn).toHaveBeenCalledTimes(1)
})
it('should remove query instance when component unmounted', async () => {
const key = queryKey()
function Page() {
useQuery(() => ({
queryKey: key,
queryFn: () => sleep(10).then(() => 'data'),
}))
return <>rendered</>
}
function App() {
const [show, setShow] = createSignal(false)
return (
<>
<Suspense fallback="loading">{show() && <Page />}</Suspense>
<button
aria-label="toggle"
onClick={() => setShow((prev) => !prev)}
/>
</>
)
}
const rendered = renderWithClient(queryClient, () => <App />)
expect(rendered.queryByText('rendered')).not.toBeInTheDocument()
expect(queryCache.find({ queryKey: key })).toBeUndefined()
fireEvent.click(rendered.getByLabelText('toggle'))
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('rendered')).toBeInTheDocument()
expect(queryCache.find({ queryKey: key })?.getObserversCount()).toBe(1)
fireEvent.click(rendered.getByLabelText('toggle'))
expect(rendered.queryByText('rendered')).not.toBeInTheDocument()
expect(queryCache.find({ queryKey: key })?.getObserversCount()).toBe(0)
})
// https://github.com/tannerlinsley/react-query/issues/468
it('should reset error state if new component instances are mounted', async () => {
const key = queryKey()
let succeed = false
function Page() {
const state = useQuery(() => ({
queryKey: key,
queryFn: () =>
sleep(10).then(() => {
if (!succeed) throw new Error('Suspense Error Bingo')
return 'data'
}),
retryDelay: 10,
suspense: true,
}))
// Suspense only triggers if used in JSX
return (
<Show when={state.data}>
<div>rendered</div>
</Show>
)
}
const rendered = renderWithClient(queryClient, () => (
<ErrorBoundary
fallback={(_err, resetSolid) => (
<div>
<div>error boundary</div>
<button
onClick={() => {
succeed = true
resetSolid()
}}
>
retry
</button>
</div>
)}
>
<Suspense fallback="loading">
<Page />
</Suspense>
</ErrorBoundary>
))
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(100)
expect(rendered.getByText('error boundary')).toBeInTheDocument()
expect(rendered.getByText('retry')).toBeInTheDocument()
fireEvent.click(rendered.getByText('retry'))
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('rendered')).toBeInTheDocument()
})
it('should retry fetch if the reset error boundary has been reset', async () => {
const key = queryKey()
let succeed = false
function Page() {
const state = useQuery(() => ({
queryKey: key,
queryFn: () =>
sleep(10).then(() => {
if (!succeed) throw new Error('Suspense Error Bingo')
return 'data'
}),
retry: false,
suspense: true,
}))
// Suspense only triggers if used in JSX
return (
<Show when={state.data}>
<div>rendered</div>
</Show>
)
}
const rendered = renderWithClient(queryClient, () => (
<ErrorBoundary
fallback={(_err, resetSolid) => (
<div>
<div>error boundary</div>
<button
onClick={() => {
resetSolid()
}}
>
retry
</button>
</div>
)}
>
<Suspense fallback="loading">
<Page />
</Suspense>
</ErrorBoundary>
))
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('error boundary')).toBeInTheDocument()
expect(rendered.getByText('retry')).toBeInTheDocument()
fireEvent.click(rendered.getByText('retry'))
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('error boundary')).toBeInTheDocument()
expect(rendered.getByText('retry')).toBeInTheDocument()
succeed = true
fireEvent.click(rendered.getByText('retry'))
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('rendered')).toBeInTheDocument()
})
it('should refetch when re-mounting', async () => {
const key = queryKey()
let count = 0
function Component() {
const result = useQuery(() => ({
queryKey: key,
queryFn: () => sleep(100).then(() => ++count),
retry: false,
suspense: true,
staleTime: 0,
}))
return (
<div>
<span>data: {result.data}</span>
<span>fetching: {result.isFetching ? 'true' : 'false'}</span>
</div>
)
}
function Page() {
const [show, setShow] = createSignal(true)
return (
<div>
<button
onClick={() => {
setShow(!show())
}}
>
{show() ? 'hide' : 'show'}
</button>
<Suspense fallback="loading">{show() && <Component />}</Suspense>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Page />)
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(100)
expect(rendered.getByText('data: 1')).toBeInTheDocument()
expect(rendered.getByText('fetching: false')).toBeInTheDocument()
expect(rendered.getByText('hide')).toBeInTheDocument()
fireEvent.click(rendered.getByText('hide'))
await vi.advanceTimersByTimeAsync(100)
expect(rendered.getByText('show')).toBeInTheDocument()
fireEvent.click(rendered.getByText('show'))
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(0)
expect(rendered.getByText('fetching: true')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(100)
expect(rendered.getByText('data: 2')).toBeInTheDocument()
expect(rendered.getByText('fetching: false')).toBeInTheDocument()
})
it('should suspend when switching to a new query', async () => {
const key1 = queryKey()
const key2 = queryKey()
function Component(props: { queryKey: Array<string> }) {
const result = useQuery(() => ({
queryKey: props.queryKey,
queryFn: () => sleep(100).then(() => props.queryKey),
retry: false,
suspense: true,
}))
return <div>data: {result.data}</div>
}
function Page() {
const [key, setKey] = createSignal(key1)
return (
<div>
<button
onClick={() => {
setKey(key2)
}}
>
switch
</button>
<Suspense fallback="loading">
<Component queryKey={key()} />
</Suspense>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Page />)
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(100)
expect(rendered.getByText(`data: ${key1}`)).toBeInTheDocument()
fireEvent.click(rendered.getByText('switch'))
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(100)
expect(rendered.getByText(`data: ${key2}`)).toBeInTheDocument()
})
it('should throw errors to the error boundary by default', async () => {
const key = queryKey()
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
function Page() {
const state = useQuery(() => ({
queryKey: key,
queryFn: () =>
sleep(10).then(() => Promise.reject(new Error('Suspense Error a1x'))),
retry: false,
suspense: true,
}))
// read state.data to trigger suspense.
createRenderEffect(() => {
state.data
})
return <div>rendered</div>
}
function App() {
return (
<ErrorBoundary
fallback={() => (
<div>
<div>error boundary</div>
</div>
)}
>
<Suspense fallback="loading">
<Page />
</Suspense>
</ErrorBoundary>
)
}
const rendered = renderWithClient(queryClient, () => <App />)
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('error boundary')).toBeInTheDocument()
consoleMock.mockRestore()
})
it('should not throw errors to the error boundary when throwOnError: false', async () => {
const key = queryKey()
function Page() {
const state = useQuery(() => ({
queryKey: key,
queryFn: () =>
sleep(10).then(() => Promise.reject(new Error('Suspense Error a2x'))),
retry: false,
throwOnError: false,
}))
// read state.data to trigger suspense.
createRenderEffect(() => {
state.data
})
return <div>rendered</div>
}
function App() {
return (
<ErrorBoundary
fallback={() => (
<div>
<div>error boundary</div>
</div>
)}
>
<Suspense fallback="loading">
<Page />
</Suspense>
</ErrorBoundary>
)
}
const rendered = renderWithClient(queryClient, () => <App />)
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('rendered')).toBeInTheDocument()
})
it('should throw errors to the error boundary when a throwOnError function returns true', async () => {
const key = queryKey()
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
function Page() {
const state = useQuery(() => ({
queryKey: key,
queryFn: () =>
sleep(10).then(() => Promise.reject(new Error('Remote Error'))),
retry: false,
throwOnError: (err) => err.message !== 'Local Error',
}))
// read state.data to trigger suspense.
createRenderEffect(() => {
state.data
})
return <div>rendered</div>
}
function App() {
return (
<ErrorBoundary
fallback={() => (
<div>
<div>error boundary</div>
</div>
)}
>
<Suspense fallback="loading">
<Page />
</Suspense>
</ErrorBoundary>
)
}
const rendered = renderWithClient(queryClient, () => <App />)
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('error boundary')).toBeInTheDocument()
consoleMock.mockRestore()
})
it('should not throw errors to the error boundary when a throwOnError function returns false', async () => {
const key = queryKey()
function Page() {
const state = useQuery(() => ({
queryKey: key,
queryFn: () =>
sleep(10).then(() => Promise.reject(new Error('Local Error'))),
retry: false,
suspense: true,
throwOnError: (err) => err.message !== 'Local Error',
}))
// read state.data to trigger suspense.
createRenderEffect(() => {
state.data
})
return <div>rendered</div>
}
function App() {
return (
<ErrorBoundary
fallback={() => (
<div>
<div>error boundary</div>
</div>
)}
>
<Suspense fallback="loading">
<Page />
</Suspense>
</ErrorBoundary>
)
}
const rendered = renderWithClient(queryClient, () => <App />)
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('rendered')).toBeInTheDocument()
})
it('should not call the queryFn when not enabled', async () => {
const key = queryKey()
const queryFn = vi.fn(() => sleep(10).then(() => '23'))
function Page() {
const [enabled, setEnabled] = createSignal(false)
const result = useQuery(() => ({
queryKey: [key],
queryFn,
suspense: true,
enabled: enabled(),
}))
return (
<div>
<button onClick={() => setEnabled(true)}>fire</button>
<h1>{result.data}</h1>
</div>
)
}
const rendered = renderWithClient(queryClient, () => (
<Suspense fallback="loading">
<Page />
</Suspense>
))
expect(rendered.getByText('loading')).toBeInTheDocument()
expect(queryFn).toHaveBeenCalledTimes(0)
await vi.advanceTimersByTimeAsync(10)
fireEvent.click(rendered.getByRole('button', { name: /fire/i }))
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByRole('heading').textContent).toBe('23')
expect(queryFn).toHaveBeenCalledTimes(1)
})
it('should error caught in error boundary without infinite loop', async () => {
const key = queryKey()
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
let succeed = true
function Page() {
const [nonce] = createSignal(0)
const queryKeys = [`${key}-${succeed}`]
const result = useQuery(() => ({
queryKey: queryKeys,
queryFn: () =>
sleep(10).then(() => {
if (!succeed) throw new Error('Suspense Error Bingo')
return nonce()
}),
retry: false,
suspense: true,
}))
return (
<div>
<span>rendered</span> <span>{result.data}</span>
<button aria-label="fail" onClick={() => queryClient.resetQueries()}>
fail
</button>
</div>
)
}
function App() {
return (
<ErrorBoundary fallback={() => <div>error boundary</div>}>
<Suspense fallback="loading">
<Page />
</Suspense>
</ErrorBoundary>
)
}
const rendered = renderWithClient(queryClient, () => <App />)
// render suspense fallback (Loading...)
expect(rendered.getByText('loading')).toBeInTheDocument()
// resolve promise -> render Page (rendered)
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('rendered')).toBeInTheDocument()
// change query key
succeed = false
// reset query -> and throw error
fireEvent.click(rendered.getByLabelText('fail'))
// render error boundary fallback (error boundary)
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('error boundary')).toBeInTheDocument()
consoleMock.mockRestore()
})
it('should error caught in error boundary without infinite loop when query keys changed', async () => {
let succeed = true
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
function Page() {
const [key, setKey] = createSignal(0)
const result = useQuery(() => ({
queryKey: [`${key()}-${succeed}`],
queryFn: async () =>
sleep(10).then(() => {
if (!succeed) throw new Error('Suspense Error Bingo')
return 'data'
}),
retry: false,
suspense: true,
}))
return (
<div>
<span>rendered</span> <span>{result.data}</span>
<button aria-label="fail" onClick={() => setKey((k) => k + 1)}>
fail
</button>
</div>
)
}
function App() {
return (
<ErrorBoundary fallback={() => <div>error boundary</div>}>
<Suspense fallback="loading">
<Page />
</Suspense>
</ErrorBoundary>
)
}
const rendered = renderWithClient(queryClient, () => <App />)
// render suspense fallback (Loading...)
expect(rendered.getByText('loading')).toBeInTheDocument()
// resolve promise -> render Page (rendered)
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('rendered')).toBeInTheDocument()
// change promise result to error
succeed = false
// change query key
fireEvent.click(rendered.getByLabelText('fail'))
expect(rendered.getByText('loading')).toBeInTheDocument()
// render error boundary fallback (error boundary)
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('error boundary')).toBeInTheDocument()
consoleMock.mockRestore()
})
it('should error caught in error boundary without infinite loop when enabled changed', async () => {
const consoleMock = vi
.spyOn(console, 'error')
.mockImplementation(() => undefined)
function Page() {
const queryKeys = '1'
const [enabled, setEnabled] = createSignal(false)
const result = useQuery<string>(() => ({
queryKey: [queryKeys],
queryFn: () =>
sleep(10).then(() =>
Promise.reject(new Error('Suspense Error Bingo')),
),
retry: false,
suspense: true,
enabled: enabled(),
}))
return (
<div>
<span>rendered</span> <span>{result.data}</span>
<button
aria-label="fail"
onClick={() => {
setEnabled(true)
}}
>
fail
</button>
</div>
)
}
function App() {
return (
<ErrorBoundary fallback={() => <div>error boundary</div>}>
<Suspense fallback="loading">
<Page />
</Suspense>
</ErrorBoundary>
)
}
const rendered = renderWithClient(queryClient, () => <App />)
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
// render empty data with 'rendered' when enabled is false
expect(rendered.getByText('rendered')).toBeInTheDocument()
// change enabled to true
fireEvent.click(rendered.getByLabelText('fail'))
// render pending fallback
expect(rendered.getByText('loading')).toBeInTheDocument()
// render error boundary fallback (error boundary)
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('error boundary')).toBeInTheDocument()
consoleMock.mockRestore()
})
it('should render the correct amount of times in Suspense mode when gcTime is set to 0', async () => {
const key = queryKey()
let state: UseQueryResult<number> | null = null
let count = 0
let renders = 0
function Page() {
state = useQuery(() => ({
queryKey: key,
queryFn: () => sleep(10).then(() => ++count),
gcTime: 0,
}))
createRenderEffect(
on([() => ({ ...state })], () => {
renders++
}),
)
return (
<div>
<span>rendered</span>
</div>
)
}
const rendered = renderWithClient(queryClient, () => (
<Suspense fallback="loading">
<Page />
</Suspense>
))
expect(rendered.getByText('loading')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(state).toMatchObject({
data: 1,
status: 'success',
})
expect(renders).toBe(2)
expect(rendered.queryByText('rendered')).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,66 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent } from '@solidjs/testing-library'
import { Show, Suspense, createSignal, startTransition } from 'solid-js'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { QueryCache, QueryClient, useQuery } from '..'
import { renderWithClient } from './utils'
describe("useQuery's in Suspense mode with transitions", () => {
let queryCache: QueryCache
let queryClient: QueryClient
beforeEach(() => {
vi.useFakeTimers()
queryCache = new QueryCache()
queryClient = new QueryClient({ queryCache })
})
afterEach(() => {
queryClient.clear()
vi.useRealTimers()
})
it('should render the content when the transition is done', async () => {
const key = queryKey()
function Suspended() {
const state = useQuery(() => ({
queryKey: key,
queryFn: () => sleep(10).then(() => true),
}))
return <Show when={state.data}>Message</Show>
}
function Page() {
const [showSignal, setShowSignal] = createSignal(false)
return (
<div>
<button
aria-label="toggle"
onClick={() =>
startTransition(() => setShowSignal((value) => !value))
}
>
{showSignal() ? 'Hide' : 'Show'}
</button>
<Suspense fallback="Loading">
<Show when={showSignal()}>
<Suspended />
</Show>
</Suspense>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Page />)
expect(rendered.getByText('Show')).toBeInTheDocument()
fireEvent.click(rendered.getByLabelText('toggle'))
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('Message')).toBeInTheDocument()
// verify that the button also updated. See https://github.com/solidjs/solid/issues/1249
expect(rendered.getByText('Hide')).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,210 @@
import { describe, expectTypeOf, it } from 'vitest'
import { queryKey } from '@tanstack/query-test-utils'
import { useInfiniteQuery } from '../useInfiniteQuery'
import { QueryClient } from '../QueryClient'
import type { InfiniteData } from '@tanstack/query-core'
describe('useInfiniteQuery', () => {
describe('pageParam', () => {
it('initialPageParam should define type of param passed to queryFunctionContext', () => {
useInfiniteQuery(() => ({
queryKey: queryKey(),
queryFn: ({ pageParam }) => {
expectTypeOf(pageParam).toEqualTypeOf<number>()
},
initialPageParam: 1,
getNextPageParam: () => undefined,
}))
})
it('direction should be passed to queryFn of useInfiniteQuery', () => {
useInfiniteQuery(() => ({
queryKey: queryKey(),
queryFn: ({ direction }) => {
expectTypeOf(direction).toEqualTypeOf<'forward' | 'backward'>()
},
initialPageParam: 1,
getNextPageParam: () => undefined,
}))
})
it('initialPageParam should define type of param passed to queryFunctionContext for fetchInfiniteQuery', () => {
const queryClient = new QueryClient()
// grandfathered direct test
// eslint-disable-next-line no-restricted-syntax
queryClient.fetchInfiniteQuery({
queryKey: queryKey(),
queryFn: ({ pageParam }) => {
expectTypeOf(pageParam).toEqualTypeOf<number>()
},
initialPageParam: 1,
})
})
it('initialPageParam should define type of param passed to queryFunctionContext for prefetchInfiniteQuery', () => {
const queryClient = new QueryClient()
// grandfathered direct test
// eslint-disable-next-line no-restricted-syntax
queryClient.prefetchInfiniteQuery({
queryKey: queryKey(),
queryFn: ({ pageParam }) => {
expectTypeOf(pageParam).toEqualTypeOf<number>()
},
initialPageParam: 1,
})
})
})
describe('initialData', () => {
it('TData should always be defined when initialData is provided', () => {
const { data } = useInfiniteQuery(() => ({
queryKey: queryKey(),
queryFn: ({ pageParam }) => {
return pageParam * 5
},
initialPageParam: 1,
getNextPageParam: () => undefined,
initialData: { pages: [5], pageParams: [1] },
}))
expectTypeOf(data).toEqualTypeOf<InfiniteData<number, unknown>>()
})
it('TData should have undefined in the union when initialData is NOT provided', () => {
const { data } = useInfiniteQuery(() => ({
queryKey: queryKey(),
queryFn: ({ pageParam }) => {
return pageParam * 5
},
initialPageParam: 1,
getNextPageParam: () => undefined,
}))
expectTypeOf(data).toEqualTypeOf<
InfiniteData<number, unknown> | undefined
>()
})
})
describe('select', () => {
it('should still return paginated data if no select result', () => {
const infiniteQuery = useInfiniteQuery(() => ({
queryKey: queryKey(),
queryFn: ({ pageParam }) => {
return pageParam * 5
},
initialPageParam: 1,
getNextPageParam: () => undefined,
}))
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
expectTypeOf(infiniteQuery.data).toEqualTypeOf<
InfiniteData<number, unknown> | undefined
>()
})
it('should be able to transform data to arbitrary result', () => {
const infiniteQuery = useInfiniteQuery(() => ({
queryKey: queryKey(),
queryFn: ({ pageParam }) => {
return pageParam * 5
},
initialPageParam: 1,
getNextPageParam: () => undefined,
select: (data) => {
expectTypeOf(data).toEqualTypeOf<InfiniteData<number, number>>()
return 'selected' as const
},
}))
expectTypeOf(infiniteQuery.data).toEqualTypeOf<'selected' | undefined>()
})
})
describe('getNextPageParam / getPreviousPageParam', () => {
it('should get typed params', () => {
const infiniteQuery = useInfiniteQuery(() => ({
queryKey: queryKey(),
queryFn: ({ pageParam }) => {
return String(pageParam)
},
initialPageParam: 1,
getNextPageParam: (
lastPage,
allPages,
lastPageParam,
allPageParams,
) => {
expectTypeOf(lastPage).toEqualTypeOf<string>()
expectTypeOf(allPages).toEqualTypeOf<Array<string>>()
expectTypeOf(lastPageParam).toEqualTypeOf<number>()
expectTypeOf(allPageParams).toEqualTypeOf<Array<number>>()
return undefined
},
getPreviousPageParam: (
firstPage,
allPages,
firstPageParam,
allPageParams,
) => {
expectTypeOf(firstPage).toEqualTypeOf<string>()
expectTypeOf(allPages).toEqualTypeOf<Array<string>>()
expectTypeOf(firstPageParam).toEqualTypeOf<number>()
expectTypeOf(allPageParams).toEqualTypeOf<Array<number>>()
return undefined
},
}))
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
expectTypeOf(infiniteQuery.data).toEqualTypeOf<
InfiniteData<string, unknown> | undefined
>()
})
})
describe('error booleans', () => {
it('should not be permanently `false`', () => {
const {
isFetchNextPageError,
isFetchPreviousPageError,
isLoadingError,
isRefetchError,
} = useInfiniteQuery(() => ({
queryKey: queryKey(),
queryFn: ({ pageParam }) => {
return pageParam * 5
},
initialPageParam: 1,
getNextPageParam: () => undefined,
}))
expectTypeOf(isFetchNextPageError).toEqualTypeOf<boolean>()
expectTypeOf(isFetchPreviousPageError).toEqualTypeOf<boolean>()
expectTypeOf(isLoadingError).toEqualTypeOf<boolean>()
expectTypeOf(isRefetchError).toEqualTypeOf<boolean>()
})
})
describe('queryClient', () => {
it('should accept queryClient as second argument', () => {
const queryClient = new QueryClient()
const infiniteQuery = useInfiniteQuery(
() => ({
queryKey: queryKey(),
queryFn: ({ pageParam }) => {
return pageParam * 5
},
initialPageParam: 1,
getNextPageParam: () => undefined,
}),
() => queryClient,
)
// TODO: Order of generics prevents pageParams to be typed correctly. Using `unknown` for now
expectTypeOf(infiniteQuery.data).toEqualTypeOf<
InfiniteData<number, unknown> | undefined
>()
})
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,292 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent, render } from '@solidjs/testing-library'
import { Show, createEffect, createRenderEffect, createSignal } from 'solid-js'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { QueryCache, QueryClient, useIsFetching, useQuery } from '..'
import { renderWithClient, setActTimeout } from './utils'
describe('useIsFetching', () => {
let queryCache: QueryCache
let queryClient: QueryClient
beforeEach(() => {
vi.useFakeTimers()
queryCache = new QueryCache()
queryClient = new QueryClient({ queryCache })
})
afterEach(() => {
queryClient.clear()
vi.useRealTimers()
})
// See https://github.com/tannerlinsley/react-query/issues/105
it('should update as queries start and stop fetching', async () => {
const key = queryKey()
function IsFetching() {
const isFetching = useIsFetching()
return <div>isFetching: {isFetching()}</div>
}
function Query() {
const [ready, setReady] = createSignal(false)
useQuery(() => ({
queryKey: key,
queryFn: () => sleep(50).then(() => 'test'),
enabled: ready(),
}))
return <button onClick={() => setReady(true)}>setReady</button>
}
function Page() {
return (
<div>
<IsFetching />
<Query />
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Page />)
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /setReady/i }))
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(50)
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
})
it('should not update state while rendering', async () => {
const key1 = queryKey()
const key2 = queryKey()
const isFetchingArray: Array<number> = []
function IsFetching() {
const isFetching = useIsFetching()
createRenderEffect(() => {
isFetchingArray.push(isFetching())
})
return null
}
function FirstQuery() {
useQuery(() => ({
queryKey: key1,
queryFn: () => sleep(150).then(() => 'data'),
}))
return null
}
function SecondQuery() {
useQuery(() => ({
queryKey: key2,
queryFn: () => sleep(200).then(() => 'data'),
}))
return null
}
function Page() {
const [renderSecond, setRenderSecond] = createSignal(false)
createEffect(() => {
setActTimeout(() => {
setRenderSecond(true)
}, 100)
})
return (
<>
<IsFetching />
<FirstQuery />
<Show when={renderSecond()}>
<SecondQuery />
</Show>
</>
)
}
renderWithClient(queryClient, () => <Page />)
// unlike react, Updating renderSecond wont cause a rerender for FirstQuery
await vi.advanceTimersByTimeAsync(300)
expect(isFetchingArray).toEqual([0, 1, 2, 1, 0])
})
it('should be able to filter', async () => {
const key1 = queryKey()
const key2 = queryKey()
const isFetchingArray: Array<number> = []
function One() {
useQuery(() => ({
queryKey: key1,
queryFn: () => sleep(10).then(() => 'test'),
}))
return null
}
function Two() {
useQuery(() => ({
queryKey: key2,
queryFn: () => sleep(20).then(() => 'test'),
}))
return null
}
function Page() {
const [started, setStarted] = createSignal(false)
const isFetching = useIsFetching(() => ({
queryKey: key1,
}))
createRenderEffect(() => {
isFetchingArray.push(isFetching())
})
return (
<div>
<button onClick={() => setStarted(true)}>setStarted</button>
<div>isFetching: {isFetching()}</div>
<Show when={started()}>
<>
<One />
<Two />
</>
</Show>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Page />)
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /setStarted/i }))
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(20)
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
// at no point should we have isFetching: 2
expect(isFetchingArray).toEqual(expect.not.arrayContaining([2]))
})
it('should show the correct fetching state when mounted after a query', async () => {
const key = queryKey()
function Page() {
useQuery(() => ({
queryKey: key,
queryFn: () => sleep(10).then(() => 'test'),
}))
const isFetching = useIsFetching()
return (
<div>
<div>isFetching: {isFetching()}</div>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Page />)
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('isFetching: 0')).toBeInTheDocument()
})
it('should use provided custom queryClient', async () => {
const customClient = new QueryClient()
const key = queryKey()
function Page() {
useQuery(
() => ({
queryKey: key,
queryFn: () => sleep(10).then(() => 'test'),
}),
() => customClient,
)
const isFetching = useIsFetching(undefined, () => customClient)
return (
<div>
<div>isFetching: {isFetching()}</div>
</div>
)
}
const rendered = render(() => <Page></Page>)
expect(rendered.getByText('isFetching: 1')).toBeInTheDocument()
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.query({
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.query({
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()
})
})

View File

@@ -0,0 +1,303 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent, render } from '@solidjs/testing-library'
import { Show, createEffect, createRenderEffect, createSignal } from 'solid-js'
import * as QueryCore from '@tanstack/query-core'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { QueryClient, useIsMutating, useMutation } from '..'
import { renderWithClient, setActTimeout } from './utils'
describe('useIsMutating', () => {
let queryClient: QueryClient
beforeEach(() => {
vi.useFakeTimers()
queryClient = new QueryClient()
})
afterEach(() => {
queryClient.clear()
vi.useRealTimers()
})
it('should return the number of fetching mutations', async () => {
const isMutatingArray: Array<number> = []
const mutationKey1 = queryKey()
const mutationKey2 = queryKey()
function IsMutating() {
const isMutating = useIsMutating()
createRenderEffect(() => {
isMutatingArray.push(isMutating())
})
return null
}
function Mutations() {
const { mutate: mutate1 } = useMutation(() => ({
mutationKey: mutationKey1,
mutationFn: () => sleep(150).then(() => 'data'),
}))
const { mutate: mutate2 } = useMutation(() => ({
mutationKey: mutationKey2,
mutationFn: () => sleep(50).then(() => 'data'),
}))
createEffect(() => {
mutate1()
setActTimeout(() => {
mutate2()
}, 50)
})
return null
}
function Page() {
return (
<div>
<IsMutating />
<Mutations />
</div>
)
}
renderWithClient(queryClient, () => <Page />)
await vi.advanceTimersByTimeAsync(150)
expect(isMutatingArray).toEqual([0, 1, 2, 1, 0])
})
it('should filter correctly by mutationKey', async () => {
const isMutatingArray: Array<number> = []
const mutationKey1 = queryKey()
const mutationKey2 = queryKey()
function IsMutating() {
const isMutating = useIsMutating(() => ({ mutationKey: mutationKey1 }))
createRenderEffect(() => {
isMutatingArray.push(isMutating())
})
return null
}
function Page() {
const { mutate: mutate1 } = useMutation(() => ({
mutationKey: mutationKey1,
mutationFn: () => sleep(100).then(() => 'data'),
}))
const { mutate: mutate2 } = useMutation(() => ({
mutationKey: mutationKey2,
mutationFn: () => sleep(100).then(() => 'data'),
}))
createEffect(() => {
mutate1()
mutate2()
})
return <IsMutating />
}
renderWithClient(queryClient, () => <Page />)
// Unlike React, IsMutating Wont re-render twice with mutation2
await vi.advanceTimersByTimeAsync(100)
expect(isMutatingArray).toEqual([0, 1, 0])
})
it('should filter correctly by predicate', async () => {
const isMutatingArray: Array<number> = []
const mutationKey1 = queryKey()
const mutationKey2 = queryKey()
function IsMutating() {
const isMutating = useIsMutating(() => ({
predicate: (mutation) =>
mutation.options.mutationKey?.[0] === mutationKey1[0],
}))
createRenderEffect(() => {
isMutatingArray.push(isMutating())
})
return null
}
function Page() {
const { mutate: mutate1 } = useMutation(() => ({
mutationKey: mutationKey1,
mutationFn: () => sleep(100).then(() => 'data'),
}))
const { mutate: mutate2 } = useMutation(() => ({
mutationKey: mutationKey2,
mutationFn: () => sleep(100).then(() => 'data'),
}))
createEffect(() => {
mutate1()
mutate2()
})
return <IsMutating />
}
renderWithClient(queryClient, () => <Page />)
// Again, No unnecessary re-renders like React
await vi.advanceTimersByTimeAsync(100)
expect(isMutatingArray).toEqual([0, 1, 0])
})
it('should use provided custom queryClient', async () => {
const customClient = new QueryClient()
const mutationKey1 = queryKey()
function Page() {
const isMutating = useIsMutating(undefined, () => customClient)
const { mutate } = useMutation(
() => ({
mutationKey: mutationKey1,
mutationFn: () => sleep(20).then(() => 'data'),
}),
() => customClient,
)
createEffect(() => {
setActTimeout(() => {
mutate()
}, 10)
})
return (
<div>
<div>mutating: {isMutating()}</div>
</div>
)
}
const rendered = render(() => <Page></Page>)
expect(rendered.getByText('mutating: 0')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('mutating: 1')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(20)
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
// the listener when the component is unmounted
class MutationCacheMock extends QueryCore.MutationCache {
subscribe(listener: any) {
super.subscribe(listener)
return () => void 0
}
}
const MutationCacheSpy = vi
.spyOn(QueryCore, 'MutationCache')
.mockImplementation((fn) => {
return new MutationCacheMock(fn)
})
// Create the client after mocking MutationCache so it uses the mock,
// not the centralized client from beforeEach
const spiedClient = new QueryClient()
const mutationKey1 = queryKey()
function IsMutating() {
useIsMutating()
return null
}
function Page() {
const [mounted, setMounted] = createSignal(true)
const { mutate: mutate1 } = useMutation(() => ({
mutationKey: mutationKey1,
mutationFn: () => sleep(10).then(() => 'data'),
}))
createEffect(() => {
mutate1()
})
return (
<div>
<button onClick={() => setMounted(false)}>unmount</button>
<Show when={mounted()}>
<IsMutating />
</Show>
</div>
)
}
const rendered = renderWithClient(spiedClient, () => <Page />)
fireEvent.click(rendered.getByText('unmount'))
// Should not display the console error
// "Warning: Can't perform a React state update on an unmounted component"
await vi.advanceTimersByTimeAsync(20)
MutationCacheSpy.mockRestore()
})
})

View File

@@ -0,0 +1,204 @@
import { describe, expectTypeOf, it } from 'vitest'
import { useMutation } from '../useMutation'
import { QueryClient } from '../QueryClient'
import type {
DefaultError,
MutationFunctionContext,
MutationKey,
QueryClient as QueryCoreClient,
} from '@tanstack/query-core'
import type { UseMutationResult } from '../types'
describe('useMutation', () => {
it('should infer TData from mutationFn return type', () => {
const mutation = useMutation(() => ({
mutationFn: () => Promise.resolve('data'),
}))
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
expectTypeOf(mutation.error).toEqualTypeOf<DefaultError | null>()
})
it('should infer TVariables from mutationFn parameter', () => {
const mutation = useMutation(() => ({
mutationFn: (vars: { id: string }) => Promise.resolve(vars.id),
}))
expectTypeOf(mutation.mutate).toBeCallableWith({ id: '1' })
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
})
it('should infer TOnMutateResult from onMutate return type', () => {
useMutation(() => ({
mutationFn: () => Promise.resolve('data'),
onMutate: () => {
return { token: 'abc' }
},
onSuccess: (_data, _variables, onMutateResult) => {
expectTypeOf(onMutateResult).toEqualTypeOf<{ token: string }>()
},
onError: (_error, _variables, onMutateResult) => {
expectTypeOf(onMutateResult).toEqualTypeOf<
{ token: string } | undefined
>()
},
}))
})
it('should allow explicit generic types', () => {
const mutation = useMutation<string, Error, { id: number }>(() => ({
mutationFn: (vars) => {
expectTypeOf(vars).toEqualTypeOf<{ id: number }>()
return Promise.resolve('result')
},
}))
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
expectTypeOf(mutation.error).toEqualTypeOf<Error | null>()
})
it('should return correct UseMutationResult type', () => {
const mutation = useMutation(() => ({
mutationFn: () => Promise.resolve(42),
}))
expectTypeOf(mutation).toEqualTypeOf<
UseMutationResult<number, DefaultError, void, unknown>
>()
})
it('should type mutateAsync with correct return type', () => {
const mutation = useMutation(() => ({
mutationFn: (id: string) => Promise.resolve(id.length),
}))
expectTypeOf(mutation.mutateAsync).toBeCallableWith('test')
expectTypeOf(mutation.mutateAsync('test')).toEqualTypeOf<Promise<number>>()
})
it('should default TVariables to void when mutationFn has no parameters', () => {
const mutation = useMutation(() => ({
mutationFn: () => Promise.resolve('data'),
}))
expectTypeOf(mutation.mutate).toBeCallableWith()
})
it('should allow calling mutate with no arguments when TVariables is optional undefinable', () => {
const mutation = useMutation(() => ({
mutationFn: (_variables: number | undefined) => Promise.resolve(1),
}))
expectTypeOf(mutation.mutate).toBeCallableWith()
expectTypeOf(mutation.mutateAsync).toBeCallableWith()
})
it('should infer custom TError type', () => {
class CustomError extends Error {
code: number
constructor(code: number) {
super()
this.code = code
}
}
const mutation = useMutation<string, CustomError>(() => ({
mutationFn: () => Promise.resolve('data'),
}))
expectTypeOf(mutation.error).toEqualTypeOf<CustomError | null>()
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
})
it('should infer types for onSettled callback', () => {
useMutation(() => ({
mutationFn: () => Promise.resolve(42),
onSettled: (data, error, _variables, _onMutateResult) => {
expectTypeOf(data).toEqualTypeOf<number | undefined>()
expectTypeOf(error).toEqualTypeOf<DefaultError | null>()
},
}))
})
it('should infer custom TError in onError callback', () => {
class CustomError extends Error {
code: number
constructor(code: number) {
super()
this.code = code
}
}
useMutation<string, CustomError>(() => ({
mutationFn: () => Promise.resolve('data'),
onError: (error) => {
expectTypeOf(error).toEqualTypeOf<CustomError>()
},
}))
})
it('should accept queryClient as second argument', () => {
const queryClient = new QueryClient()
const mutation = useMutation(
() => ({
mutationFn: () => Promise.resolve('data'),
}),
() => queryClient,
)
expectTypeOf(mutation.data).toEqualTypeOf<string | undefined>()
})
it('should type context as the last argument for mutationFn and every hook-level callback', () => {
useMutation(() => ({
mutationFn: (_variables, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
expectTypeOf(context.client).toEqualTypeOf<QueryCoreClient>()
return Promise.resolve('data')
},
onMutate: (_variables, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onSuccess: (_data, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onError: (_error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
}))
})
it('should type context as the last argument for every per-call mutate option', () => {
const mutation = useMutation(() => ({
mutationFn: () => Promise.resolve('data'),
}))
mutation.mutate(undefined, {
onSuccess: (_data, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onError: (_error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
})
})
it('should type context.mutationKey as MutationKey', () => {
useMutation(() => ({
mutationKey: ['todos', 'add'] as const,
mutationFn: () => Promise.resolve('data'),
onSuccess: (_data, _variables, _onMutateResult, context) => {
expectTypeOf(context.mutationKey).toEqualTypeOf<
MutationKey | undefined
>()
},
}))
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
import { describe, expectTypeOf, it } from 'vitest'
import { useMutationState } from '../useMutationState'
import type {
Mutation,
MutationState,
MutationStatus,
} from '@tanstack/query-core'
describe('useMutationState', () => {
it('should default to QueryState', () => {
const result = useMutationState(() => ({
filters: { status: 'pending' },
}))
expectTypeOf(result()).toEqualTypeOf<Array<MutationState>>()
})
it('should infer with select', () => {
const result = useMutationState(() => ({
filters: { status: 'pending' },
select: (mutation) => mutation.state.status,
}))
expectTypeOf(result()).toEqualTypeOf<Array<MutationStatus>>()
})
it('should propagate generics to select callback when TResult is typed MutationState', () => {
type MyData = { data: Array<string> }
type MyError = { code: number; message: string }
type MyVars = { id: number }
const result = useMutationState<MutationState<MyData, MyError, MyVars>>(
() => ({
filters: { mutationKey: ['key'] },
select: (mutation) => {
expectTypeOf(mutation).toEqualTypeOf<
Mutation<MyData, MyError, MyVars, unknown>
>()
return mutation.state
},
}),
)
expectTypeOf(result()).toEqualTypeOf<
Array<MutationState<MyData, MyError, MyVars, unknown>>
>()
})
})

View File

@@ -0,0 +1,114 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent } from '@solidjs/testing-library'
import { createEffect } from 'solid-js'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { QueryClient, useMutation, useMutationState } from '..'
import { renderWithClient } from './utils'
describe('useMutationState', () => {
let queryClient: QueryClient
beforeEach(() => {
vi.useFakeTimers()
queryClient = new QueryClient()
})
afterEach(() => {
queryClient.clear()
vi.useRealTimers()
})
it('should return all mutation states when called without options', async () => {
const mutationKey = queryKey()
function States() {
const mutationStates = useMutationState()
return <div>count: {mutationStates().length}</div>
}
function Mutate() {
const mutation = useMutation(() => ({
mutationKey,
mutationFn: (input: number) => sleep(150).then(() => 'data' + input),
}))
return (
<div>
<button onClick={() => mutation.mutate(1)}>mutate</button>
</div>
)
}
function Page() {
return (
<div>
<States />
<Mutate />
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Page />)
expect(rendered.getByText('count: 0')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(0)
expect(rendered.getByText('count: 1')).toBeInTheDocument()
await vi.advanceTimersByTimeAsync(150)
expect(rendered.getByText('count: 1')).toBeInTheDocument()
})
it('should return variables after calling mutate', async () => {
const variables: Array<Array<unknown>> = []
const mutationKey = queryKey()
function Variables() {
const states = useMutationState(() => ({
filters: { mutationKey, status: 'pending' },
select: (mutation) => mutation.state.variables,
}))
createEffect(() => {
variables.push(states())
})
return null
}
function Mutate() {
const mutation = useMutation(() => ({
mutationKey,
mutationFn: (input: number) => sleep(150).then(() => 'data' + input),
}))
return (
<div>
data: {mutation.data ?? 'null'}
<button onClick={() => mutation.mutate(1)}>mutate</button>
</div>
)
}
function Page() {
return (
<div>
<Variables />
<Mutate />
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Page />)
expect(rendered.getByText('data: null')).toBeInTheDocument()
fireEvent.click(rendered.getByRole('button', { name: /mutate/i }))
await vi.advanceTimersByTimeAsync(150)
expect(rendered.getByText('data: data1')).toBeInTheDocument()
expect(variables).toEqual([[], [1], []])
})
})

View File

@@ -0,0 +1,894 @@
import { describe, expectTypeOf, it } from 'vitest'
import { skipToken } from '@tanstack/query-core'
import { queryKey } from '@tanstack/query-test-utils'
import { queryOptions, useQueries } from '..'
import { QueryClient } from '../QueryClient'
import type * as QueryCore from '@tanstack/query-core'
import type { OmitKeyof } from '@tanstack/query-core'
import type {
QueryFunction,
QueryFunctionContext,
QueryKey,
UseQueryResult,
} from '..'
import type { QueryOptions } from '../types'
describe('useQueries', () => {
it('TData should have undefined in the union even when initialData is provided as an object', () => {
const query1 = {
queryKey: queryKey(),
queryFn: () => {
return {
wow: true,
}
},
initialData: {
wow: false,
},
}
const query2 = {
queryKey: queryKey(),
queryFn: () => 'Query Data',
initialData: 'initial data',
}
const query3 = {
queryKey: queryKey(),
queryFn: () => 'Query Data',
}
const queryResults = useQueries(() => ({
queries: [query1, query2, query3],
}))
const query1Data = queryResults[0].data
const query2Data = queryResults[1].data
const query3Data = queryResults[2].data
expectTypeOf(query1Data).toEqualTypeOf<{ wow: boolean } | undefined>()
expectTypeOf(query2Data).toEqualTypeOf<string | undefined>()
expectTypeOf(query3Data).toEqualTypeOf<string | undefined>()
})
it('TData should have undefined in the union when passed through queryOptions', () => {
const options = queryOptions({
queryKey: queryKey(),
queryFn: () => {
return {
wow: true,
}
},
initialData: {
wow: true,
},
})
const queryResults = useQueries(() => ({ queries: [options] }))
const data = queryResults[0].data
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
})
it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
const queryResults = useQueries(() => ({
queries: [
{
queryKey: queryKey(),
queryFn: () => {
return {
wow: true,
}
},
initialData: () => undefined as { wow: boolean } | undefined,
},
],
}))
const data = queryResults[0].data
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
})
it('should infer types from explicit object type parameter', () => {
const queryResults = useQueries<
[
{ queryFnData: number },
{ queryFnData: string; error: Error },
{ queryFnData: boolean; data: string },
]
>(() => ({
queries: [
{
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
},
{
queryKey: queryKey(),
queryFn: () => Promise.resolve('data'),
},
{
queryKey: queryKey(),
queryFn: () => Promise.resolve(true),
select: () => 'selected',
},
],
}))
expectTypeOf(queryResults[0].data).toEqualTypeOf<number | undefined>()
expectTypeOf(queryResults[1].data).toEqualTypeOf<string | undefined>()
expectTypeOf(queryResults[1].error).toEqualTypeOf<Error | null>()
expectTypeOf(queryResults[2].data).toEqualTypeOf<string | undefined>()
})
it('should infer types from explicit tuple type parameter', () => {
const queryResults = useQueries<
[[number], [string, Error], [boolean, Error, string]]
>(() => ({
queries: [
{
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
},
{
queryKey: queryKey(),
queryFn: () => Promise.resolve('data'),
},
{
queryKey: queryKey(),
queryFn: () => Promise.resolve(true),
select: () => 'selected',
},
],
}))
expectTypeOf(queryResults[0].data).toEqualTypeOf<number | undefined>()
expectTypeOf(queryResults[1].data).toEqualTypeOf<string | undefined>()
expectTypeOf(queryResults[1].error).toEqualTypeOf<Error | null>()
expectTypeOf(queryResults[2].data).toEqualTypeOf<string | undefined>()
})
it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQueries', () => {
const query1 = queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
select: (data) => data > 1,
})
const query2 = {
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
select: (data: number) => data > 1,
}
const queryResults = useQueries(() => ({ queries: [query1, query2] }))
const query1Data = queryResults[0].data
const query2Data = queryResults[1].data
expectTypeOf(query1Data).toEqualTypeOf<boolean | undefined>()
expectTypeOf(query2Data).toEqualTypeOf<boolean | undefined>()
})
describe('custom hook', () => {
it('should allow custom hooks using QueryOptions', () => {
type Data = string
const useCustomQueries = (
options?: OmitKeyof<QueryOptions<Data>, 'queryKey' | 'queryFn'>,
) => {
return useQueries(() => ({
queries: [
{
...options,
queryKey: queryKey(),
queryFn: () => Promise.resolve('data'),
},
],
}))
}
const queryResults = useCustomQueries()
const data = queryResults[0].data
expectTypeOf(data).toEqualTypeOf<Data | undefined>()
})
})
it('should infer custom TError from throwOnError', () => {
class CustomError extends Error {
code: number
constructor(code: number) {
super()
this.code = code
}
}
const queryResults = useQueries(() => ({
queries: [
{
queryKey: queryKey(),
queryFn: () => Promise.resolve('data'),
throwOnError: (_error: CustomError) => false,
},
],
}))
expectTypeOf(queryResults[0].error).toEqualTypeOf<CustomError | null>()
})
it('TData should have correct type when conditional skipToken is passed', () => {
const queryResults = useQueries(() => ({
queries: [
{
queryKey: queryKey(),
queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5),
},
],
}))
const firstResult = queryResults[0]
expectTypeOf(firstResult).toEqualTypeOf<UseQueryResult<number, Error>>()
expectTypeOf(firstResult.data).toEqualTypeOf<number | undefined>()
})
it('should return correct data for dynamic queries with mixed result types', () => {
const Queries1 = {
get: () =>
queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
}),
}
const Queries2 = {
get: () =>
queryOptions({
queryKey: queryKey(),
queryFn: () => Promise.resolve(true),
}),
}
const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() }))
const result = useQueries(() => ({
queries: [...queries1List, { ...Queries2.get() }],
}))
expectTypeOf(result).toEqualTypeOf<
[...Array<UseQueryResult<number, Error>>, UseQueryResult<boolean, Error>]
>()
})
it('should accept queryClient as second argument', () => {
const queryClient = new QueryClient()
const queryResults = useQueries(
() => ({
queries: [
{
queryKey: queryKey(),
queryFn: () => Promise.resolve('data'),
},
],
}),
() => queryClient,
)
expectTypeOf(queryResults[0].data).toEqualTypeOf<string | undefined>()
})
it('should infer correct types for combine callback parameter', () => {
useQueries(() => ({
queries: [
{
queryKey: queryKey(),
queryFn: () => Promise.resolve(1),
},
{
queryKey: queryKey(),
queryFn: () => Promise.resolve('hello'),
},
],
combine: (results) => {
expectTypeOf(results[0]).toEqualTypeOf<UseQueryResult<number, Error>>()
expectTypeOf(results[1]).toEqualTypeOf<UseQueryResult<string, Error>>()
return results
},
}))
})
describe('type parameters', () => {
it('should handle type parameter - tuple of tuples', () => {
const key1 = queryKey()
const key2 = queryKey()
const key3 = queryKey()
// @ts-expect-error (Page component is not rendered)
function Page() {
const result1 = useQueries<
[[number], [string], [Array<string>, boolean]]
>(() => ({
queries: [
{
queryKey: key1,
queryFn: () => 1,
},
{
queryKey: key2,
queryFn: () => 'string',
},
{
queryKey: key3,
queryFn: () => ['string[]'],
},
],
}))
expectTypeOf(result1[0]).toEqualTypeOf<UseQueryResult<number>>()
expectTypeOf(result1[1]).toEqualTypeOf<UseQueryResult<string>>()
expectTypeOf(result1[2]).toEqualTypeOf<
UseQueryResult<Array<string>, boolean>
>()
expectTypeOf(result1[0].data).toEqualTypeOf<number | undefined>()
expectTypeOf(result1[1].data).toEqualTypeOf<string | undefined>()
expectTypeOf(result1[2].data).toEqualTypeOf<Array<string> | undefined>()
expectTypeOf(result1[2].error).toEqualTypeOf<boolean | null>()
// TData (3rd element) takes precedence over TQueryFnData (1st element)
const result2 = useQueries<
[[string, unknown, string], [string, unknown, number]]
>(() => ({
queries: [
{
queryKey: key1,
queryFn: () => 'string',
select: (a) => {
expectTypeOf(a).toEqualTypeOf<string>()
return a.toLowerCase()
},
},
{
queryKey: key2,
queryFn: () => 'string',
select: (a) => {
expectTypeOf(a).toEqualTypeOf<string>()
return parseInt(a)
},
},
],
}))
expectTypeOf(result2[0]).toEqualTypeOf<
UseQueryResult<string, unknown>
>()
expectTypeOf(result2[1]).toEqualTypeOf<
UseQueryResult<number, unknown>
>()
expectTypeOf(result2[0].data).toEqualTypeOf<string | undefined>()
expectTypeOf(result2[1].data).toEqualTypeOf<number | undefined>()
// types should be enforced
useQueries<[[string, unknown, string], [string, boolean, number]]>(
() => ({
queries: [
{
queryKey: key1,
queryFn: () => 'string',
select: (a) => {
expectTypeOf(a).toEqualTypeOf<string>()
return a.toLowerCase()
},
placeholderData: 'string',
// @ts-expect-error (initialData: string)
initialData: 123,
},
{
queryKey: key2,
queryFn: () => 'string',
select: (a) => {
expectTypeOf(a).toEqualTypeOf<string>()
return parseInt(a)
},
placeholderData: 'string',
// @ts-expect-error (initialData: string)
initialData: 123,
},
],
}),
)
// field names should be enforced
useQueries<[[string]]>(() => ({
queries: [
{
queryKey: key1,
queryFn: () => 'string',
},
],
}))
}
})
it('should handle type parameter - tuple of objects', () => {
const key1 = queryKey()
const key2 = queryKey()
const key3 = queryKey()
// @ts-expect-error (Page component is not rendered)
function Page() {
const result1 = useQueries<
[
{ queryFnData: number },
{ queryFnData: string },
{ queryFnData: Array<string>; error: boolean },
]
>(() => ({
queries: [
{
queryKey: key1,
queryFn: () => 1,
},
{
queryKey: key2,
queryFn: () => 'string',
},
{
queryKey: key3,
queryFn: () => ['string[]'],
},
],
}))
expectTypeOf(result1[0]).toEqualTypeOf<
UseQueryResult<number, unknown>
>()
expectTypeOf(result1[1]).toEqualTypeOf<
UseQueryResult<string, unknown>
>()
expectTypeOf(result1[2]).toEqualTypeOf<
UseQueryResult<Array<string>, boolean>
>()
expectTypeOf(result1[0].data).toEqualTypeOf<number | undefined>()
expectTypeOf(result1[1].data).toEqualTypeOf<string | undefined>()
expectTypeOf(result1[2].data).toEqualTypeOf<Array<string> | undefined>()
expectTypeOf(result1[2].error).toEqualTypeOf<boolean | null>()
// TData (data prop) takes precedence over TQueryFnData (queryFnData prop)
const result2 = useQueries<
[
{ queryFnData: string; data: string },
{ queryFnData: string; data: number },
]
>(() => ({
queries: [
{
queryKey: key1,
queryFn: () => 'string',
select: (a) => {
expectTypeOf(a).toEqualTypeOf<string>()
return a.toLowerCase()
},
},
{
queryKey: key2,
queryFn: () => 'string',
select: (a) => {
expectTypeOf(a).toEqualTypeOf<string>()
return parseInt(a)
},
},
],
}))
expectTypeOf(result2[0]).toEqualTypeOf<
UseQueryResult<string, unknown>
>()
expectTypeOf(result2[1]).toEqualTypeOf<
UseQueryResult<number, unknown>
>()
expectTypeOf(result2[0].data).toEqualTypeOf<string | undefined>()
expectTypeOf(result2[1].data).toEqualTypeOf<number | undefined>()
// can pass only TData (data prop) although TQueryFnData will be left unknown
const result3 = useQueries<[{ data: string }, { data: number }]>(
() => ({
queries: [
{
queryKey: key1,
queryFn: () => 'string',
select: (a) => {
expectTypeOf(a).toEqualTypeOf<unknown>()
return a as string
},
},
{
queryKey: key2,
queryFn: () => 'string',
select: (a) => {
expectTypeOf(a).toEqualTypeOf<unknown>()
return a as number
},
},
],
}),
)
expectTypeOf(result3[0]).toEqualTypeOf<
UseQueryResult<string, unknown>
>()
expectTypeOf(result3[1]).toEqualTypeOf<
UseQueryResult<number, unknown>
>()
expectTypeOf(result3[0].data).toEqualTypeOf<string | undefined>()
expectTypeOf(result3[1].data).toEqualTypeOf<number | undefined>()
// types should be enforced
useQueries<
[
{ queryFnData: string; data: string },
{ queryFnData: string; data: number; error: boolean },
]
>(() => ({
queries: [
{
queryKey: key1,
queryFn: () => 'string',
select: (a) => {
expectTypeOf(a).toEqualTypeOf<string>()
return a.toLowerCase()
},
placeholderData: 'string',
// @ts-expect-error (initialData: string)
initialData: 123,
},
{
queryKey: key2,
queryFn: () => 'string',
select: (a) => {
expectTypeOf(a).toEqualTypeOf<string>()
return parseInt(a)
},
placeholderData: 'string',
// @ts-expect-error (initialData: string)
initialData: 123,
},
],
}))
// field names should be enforced
useQueries<[{ queryFnData: string }]>(() => ({
queries: [
{
queryKey: key1,
queryFn: () => 'string',
},
],
}))
}
})
it('should handle array literal without type parameter to infer result type', () => {
const key1 = queryKey()
const key2 = queryKey()
const key3 = queryKey()
const key4 = queryKey()
// @ts-expect-error (Page component is not rendered)
function Page() {
// Array.map preserves TQueryFnData
const result1 = useQueries(() => ({
queries: Array(50).map((_, i) => ({
queryKey: ['key', i] as const,
queryFn: () => i + 10,
})),
}))
expectTypeOf(result1).toEqualTypeOf<
Array<UseQueryResult<number, Error>>
>()
if (result1[0]) {
expectTypeOf(result1[0].data).toEqualTypeOf<number | undefined>()
}
// Array.map preserves TData
const result2 = useQueries(() => ({
queries: Array(50).map((_, i) => ({
queryKey: ['key', i] as const,
queryFn: () => i + 10,
select: (data: number) => data.toString(),
})),
}))
expectTypeOf(result2).toEqualTypeOf<
Array<UseQueryResult<string, Error>>
>()
const result3 = useQueries(() => ({
queries: [
{
queryKey: key1,
queryFn: () => 1,
},
{
queryKey: key2,
queryFn: () => 'string',
},
{
queryKey: key3,
queryFn: () => ['string[]'],
select: () => 123,
},
],
}))
expectTypeOf(result3[0]).toEqualTypeOf<UseQueryResult<number, Error>>()
expectTypeOf(result3[1]).toEqualTypeOf<UseQueryResult<string, Error>>()
expectTypeOf(result3[2]).toEqualTypeOf<UseQueryResult<number, Error>>()
expectTypeOf(result3[0].data).toEqualTypeOf<number | undefined>()
expectTypeOf(result3[1].data).toEqualTypeOf<string | undefined>()
// select takes precedence over queryFn
expectTypeOf(result3[2].data).toEqualTypeOf<number | undefined>()
// initialData/placeholderData are enforced
useQueries(() => ({
queries: [
{
queryKey: key1,
queryFn: () => 'string',
placeholderData: 'string',
// @ts-expect-error (initialData: string)
initialData: 123,
},
{
queryKey: key2,
queryFn: () => 123,
// @ts-expect-error (placeholderData: number)
placeholderData: 'string',
initialData: 123,
},
],
}))
// select params are "indirectly" enforced
useQueries(() => ({
queries: [
// unfortunately TS will not suggest the type for you
{
queryKey: key1,
queryFn: () => 'string',
},
// however you can add a type to the callback
{
queryKey: key2,
queryFn: () => 'string',
},
// the type you do pass is enforced
{
queryKey: key3,
queryFn: () => 'string',
},
{
queryKey: key4,
queryFn: () => 'string',
select: (a: string) => parseInt(a),
},
],
}))
// callbacks are also indirectly enforced with Array.map
useQueries(() => ({
queries: Array(50).map((_, i) => ({
queryKey: ['key', i] as const,
queryFn: () => i + 10,
select: (data: number) => data.toString(),
})),
}))
useQueries(() => ({
queries: Array(50).map((_, i) => ({
queryKey: ['key', i] as const,
queryFn: () => i + 10,
select: (data: number) => data.toString(),
})),
}))
// results inference works when all the handlers are defined
const result4 = useQueries(() => ({
queries: [
{
queryKey: key1,
queryFn: () => 'string',
},
{
queryKey: key2,
queryFn: () => 'string',
},
{
queryKey: key4,
queryFn: () => 'string',
select: (a: string) => parseInt(a),
},
],
}))
expectTypeOf(result4[0]).toEqualTypeOf<UseQueryResult<string, Error>>()
expectTypeOf(result4[1]).toEqualTypeOf<UseQueryResult<string, Error>>()
expectTypeOf(result4[2]).toEqualTypeOf<UseQueryResult<number, Error>>()
// handles when queryFn returns a Promise
const result5 = useQueries(() => ({
queries: [
{
queryKey: key1,
queryFn: () => Promise.resolve('string'),
},
],
}))
expectTypeOf(result5[0]).toEqualTypeOf<UseQueryResult<string, Error>>()
// Array as const does not throw error
const result6 = useQueries(
() =>
({
queries: [
{
queryKey: ['key1'],
queryFn: () => 'string',
},
{
queryKey: ['key1'],
queryFn: () => 123,
},
],
}) as const,
)
expectTypeOf(result6[0]).toEqualTypeOf<UseQueryResult<string, Error>>()
expectTypeOf(result6[1]).toEqualTypeOf<UseQueryResult<number, Error>>()
// field names should be enforced - array literal
useQueries(() => ({
queries: [
{
queryKey: key1,
queryFn: () => 'string',
},
],
}))
// field names should be enforced - Array.map() result
useQueries(() => ({
// @ts-expect-error (invalidField)
queries: Array(10).map(() => ({
someInvalidField: '',
})),
}))
// field names should be enforced - array literal
useQueries(() => ({
queries: [
{
queryKey: key1,
queryFn: () => 'string',
},
],
}))
// supports queryFn using fetch() to return Promise<any> - Array.map() result
useQueries(() => ({
queries: Array(50).map((_, i) => ({
queryKey: ['key', i] as const,
queryFn: () =>
fetch('return Promise<any>').then((resp) => resp.json()),
})),
}))
// supports queryFn using fetch() to return Promise<any> - array literal
useQueries(() => ({
queries: [
{
queryKey: key1,
queryFn: () =>
fetch('return Promise<any>').then((resp) => resp.json()),
},
],
}))
}
})
it('should handle strongly typed queryFn factories and useQueries wrappers', () => {
// QueryKey + queryFn factory
type QueryKeyA = ['queryA']
const getQueryKeyA = (): QueryKeyA => ['queryA']
type GetQueryFunctionA = () => QueryFunction<number, QueryKeyA>
const getQueryFunctionA: GetQueryFunctionA = () => () => {
return 1
}
type SelectorA = (data: number) => [number, string]
const getSelectorA = (): SelectorA => (data) => [data, data.toString()]
type QueryKeyB = ['queryB', string]
const getQueryKeyB = (id: string): QueryKeyB => ['queryB', id]
type GetQueryFunctionB = () => QueryFunction<string, QueryKeyB>
const getQueryFunctionB: GetQueryFunctionB = () => () => {
return '1'
}
type SelectorB = (data: string) => [string, number]
const getSelectorB = (): SelectorB => (data) => [data, +data]
// Wrapper with strongly typed array-parameter
function useWrappedQueries<
TQueryFnData,
TError,
TData,
TQueryKey extends QueryKey,
>(queries: Array<QueryOptions<TQueryFnData, TError, TData, TQueryKey>>) {
return useQueries(() => ({
queries: queries.map(
// no need to type the mapped query
(query) => {
const { queryFn: fn, queryKey: key } = query
expectTypeOf(fn).toEqualTypeOf<
| typeof QueryCore.skipToken
| QueryCore.QueryFunction<TQueryFnData, TQueryKey, never>
| undefined
>()
return {
queryKey: key,
queryFn: fn
? (ctx: QueryFunctionContext<TQueryKey>) => {
// eslint-disable-next-line vitest/valid-expect
expectTypeOf<TQueryKey>(ctx.queryKey)
return (
fn as QueryFunction<TQueryFnData, TQueryKey>
).call({}, ctx)
}
: undefined,
}
},
),
}))
}
// @ts-expect-error (Page component is not rendered)
function Page() {
const result = useQueries(() => ({
queries: [
{
queryKey: getQueryKeyA(),
queryFn: getQueryFunctionA(),
},
{
queryKey: getQueryKeyB('id'),
queryFn: getQueryFunctionB(),
},
],
}))
expectTypeOf(result[0]).toEqualTypeOf<UseQueryResult<number, Error>>()
expectTypeOf(result[1]).toEqualTypeOf<UseQueryResult<string, Error>>()
const withSelector = useQueries(() => ({
queries: [
{
queryKey: getQueryKeyA(),
queryFn: getQueryFunctionA(),
select: getSelectorA(),
},
{
queryKey: getQueryKeyB('id'),
queryFn: getQueryFunctionB(),
select: getSelectorB(),
},
],
}))
expectTypeOf(withSelector[0]).toEqualTypeOf<
UseQueryResult<[number, string], Error>
>()
expectTypeOf(withSelector[1]).toEqualTypeOf<
UseQueryResult<[string, number], Error>
>()
const withWrappedQueries = useWrappedQueries(
Array(10).map(() => ({
queryKey: getQueryKeyA(),
queryFn: getQueryFunctionA(),
select: getSelectorA(),
})),
)
expectTypeOf(withWrappedQueries).toEqualTypeOf<
Array<UseQueryResult<number, Error>>
>()
}
})
})
})

View File

@@ -0,0 +1,216 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { fireEvent, render } from '@solidjs/testing-library'
import * as QueryCore from '@tanstack/query-core'
import { createRenderEffect, createSignal } from 'solid-js'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import {
IsRestoringProvider,
QueriesObserver,
QueryCache,
QueryClient,
useQueries,
} from '..'
import { renderWithClient } from './utils'
import type { UseQueryResult } from '..'
describe('useQueries', () => {
let queryCache: QueryCache
let queryClient: QueryClient
beforeEach(() => {
vi.useFakeTimers()
queryCache = new QueryCache()
queryClient = new QueryClient({ queryCache })
})
afterEach(() => {
queryClient.clear()
vi.useRealTimers()
})
it('should return the correct states', async () => {
const key1 = queryKey()
const key2 = queryKey()
const results: Array<Array<UseQueryResult>> = []
function Page() {
const result = useQueries(() => ({
queries: [
{
queryKey: key1,
queryFn: () => sleep(10).then(() => 1),
},
{
queryKey: key2,
queryFn: () => sleep(100).then(() => 2),
},
],
}))
createRenderEffect(() => {
results.push([{ ...result[0] }, { ...result[1] }])
})
return (
<div>
<div>
data1: {String(result[0].data ?? 'null')}, data2:{' '}
{String(result[1].data ?? 'null')}
</div>
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Page />)
await vi.advanceTimersByTimeAsync(100)
expect(rendered.getByText('data1: 1, data2: 2')).toBeInTheDocument()
expect(results.length).toBe(3)
expect(results[0]).toMatchObject([{ data: undefined }, { data: undefined }])
expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }])
expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }])
})
// eslint-disable-next-line vitest/expect-expect
it('should not change state if unmounted', async () => {
const key1 = queryKey()
// We have to mock the QueriesObserver to not unsubscribe
// the listener when the component is unmounted
class QueriesObserverMock extends QueriesObserver {
subscribe(listener: any) {
super.subscribe(listener)
return () => void 0
}
}
const QueriesObserverSpy = vi
.spyOn(QueryCore, 'QueriesObserver')
.mockImplementation(function (
client: InstanceType<typeof QueryCore.QueryClient>,
queries: Array<QueryCore.QueryObserverOptions>,
) {
return new QueriesObserverMock(client, queries)
})
function Queries() {
useQueries(() => ({
queries: [
{
queryKey: key1,
queryFn: () => sleep(10).then(() => 1),
},
],
}))
return (
<div>
<span>queries</span>
</div>
)
}
function Page() {
const [mounted, setMounted] = createSignal(true)
return (
<div>
<button onClick={() => setMounted(false)}>unmount</button>
{mounted() && <Queries />}
</div>
)
}
const rendered = renderWithClient(queryClient, () => <Page />)
fireEvent.click(rendered.getByText('unmount'))
// Should not display the console error
// "Warning: Can't perform a React state update on an unmounted component"
await vi.advanceTimersByTimeAsync(20)
QueriesObserverSpy.mockRestore()
})
it('should use provided custom queryClient', async () => {
const key = queryKey()
const queryFn = () => sleep(10).then(() => 'custom client')
function Page() {
const queries = useQueries(
() => ({
queries: [
{
queryKey: key,
queryFn,
},
],
}),
() => queryClient,
)
return <div>data: {queries[0].data}</div>
}
const rendered = render(() => <Page />)
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByText('data: custom client')).toBeInTheDocument()
})
it('should not fetch for the duration of the restoring period when isRestoring is true', async () => {
const key1 = queryKey()
const key2 = queryKey()
const queryFn1 = vi.fn(() => sleep(10).then(() => 'data1'))
const queryFn2 = vi.fn(() => sleep(10).then(() => 'data2'))
function Page() {
const results = useQueries(() => ({
queries: [
{ queryKey: key1, queryFn: queryFn1 },
{ queryKey: key2, queryFn: queryFn2 },
],
}))
return (
<div>
<div data-testid="status1">{results[0].status}</div>
<div data-testid="status2">{results[1].status}</div>
<div data-testid="fetchStatus1">{results[0].fetchStatus}</div>
<div data-testid="fetchStatus2">{results[1].fetchStatus}</div>
<div data-testid="data1">{results[0].data ?? 'undefined'}</div>
<div data-testid="data2">{results[1].data ?? 'undefined'}</div>
</div>
)
}
const rendered = renderWithClient(queryClient, () => (
<IsRestoringProvider value={() => true}>
<Page />
</IsRestoringProvider>
))
await vi.advanceTimersByTimeAsync(0)
expect(rendered.getByTestId('status1')).toHaveTextContent('pending')
expect(rendered.getByTestId('status2')).toHaveTextContent('pending')
expect(rendered.getByTestId('fetchStatus1')).toHaveTextContent('idle')
expect(rendered.getByTestId('fetchStatus2')).toHaveTextContent('idle')
expect(rendered.getByTestId('data1')).toHaveTextContent('undefined')
expect(rendered.getByTestId('data2')).toHaveTextContent('undefined')
expect(queryFn1).toHaveBeenCalledTimes(0)
expect(queryFn2).toHaveBeenCalledTimes(0)
await vi.advanceTimersByTimeAsync(10)
expect(rendered.getByTestId('status1')).toHaveTextContent('pending')
expect(rendered.getByTestId('status2')).toHaveTextContent('pending')
expect(rendered.getByTestId('fetchStatus1')).toHaveTextContent('idle')
expect(rendered.getByTestId('fetchStatus2')).toHaveTextContent('idle')
expect(rendered.getByTestId('data1')).toHaveTextContent('undefined')
expect(rendered.getByTestId('data2')).toHaveTextContent('undefined')
expect(queryFn1).toHaveBeenCalledTimes(0)
expect(queryFn2).toHaveBeenCalledTimes(0)
})
})

View File

@@ -0,0 +1,310 @@
import { describe, expectTypeOf, it } from 'vitest'
import { queryKey } from '@tanstack/query-test-utils'
import { queryOptions, useQuery } from '../index'
import type { OmitKeyof, QueryFunction, UseQueryOptions } from '..'
describe('useQuery', () => {
const key = queryKey()
// unspecified query function should default to unknown
const noQueryFn = useQuery(() => ({ queryKey: key }))
expectTypeOf(noQueryFn.data).toEqualTypeOf<unknown>()
expectTypeOf(noQueryFn.error).toEqualTypeOf<Error | null>()
// it should infer the result type from the query function
const fromQueryFn = useQuery(() => ({
queryKey: key,
queryFn: () => 'test',
}))
expectTypeOf(fromQueryFn.data).toEqualTypeOf<string | undefined>()
expectTypeOf(fromQueryFn.error).toEqualTypeOf<Error | null>()
// it should be possible to specify the result type
const withResult = useQuery<string>(() => ({
queryKey: key,
queryFn: () => 'test',
}))
expectTypeOf(withResult.data).toEqualTypeOf<string | undefined>()
expectTypeOf(withResult.error).toEqualTypeOf<Error | null>()
// it should be possible to specify the error type
const withError = useQuery<string, Error>(() => ({
queryKey: key,
queryFn: () => 'test',
}))
expectTypeOf(withError.data).toEqualTypeOf<string | undefined>()
expectTypeOf(withError.error).toEqualTypeOf<Error | null>()
// it should provide the result type in the configuration
useQuery(() => ({
queryKey: [key],
queryFn: () => true,
}))
// it should be possible to specify a union type as result type
const unionTypeSync = useQuery(() => ({
queryKey: key,
queryFn: () => (Math.random() > 0.5 ? ('a' as const) : ('b' as const)),
}))
expectTypeOf(unionTypeSync.data).toEqualTypeOf<'a' | 'b' | undefined>()
const unionTypeAsync = useQuery<'a' | 'b'>(() => ({
queryKey: key,
queryFn: () => Promise.resolve(Math.random() > 0.5 ? 'a' : 'b'),
}))
expectTypeOf(unionTypeAsync.data).toEqualTypeOf<'a' | 'b' | undefined>()
// should error when the query function result does not match with the specified type
// @ts-expect-error
useQuery<number>(() => ({ queryKey: key, queryFn: () => 'test' }))
// it should infer the result type from a generic query function
function queryFn<T = string>(): Promise<T> {
return Promise.resolve({} as T)
}
const fromGenericQueryFn = useQuery(() => ({
queryKey: key,
queryFn: () => queryFn(),
}))
expectTypeOf(fromGenericQueryFn.data).toEqualTypeOf<string | undefined>()
expectTypeOf(fromGenericQueryFn.error).toEqualTypeOf<Error | null>()
type MyData = number
type MyQueryKey = readonly ['my-data', number]
const getMyDataArrayKey: QueryFunction<MyData, MyQueryKey> = ({
queryKey: [, n],
}) => {
return n + 42
}
useQuery(() => ({
queryKey: ['my-data', 100] as const,
queryFn: getMyDataArrayKey,
}))
const getMyDataStringKey: QueryFunction<MyData, ['1']> = (context) => {
expectTypeOf(context.queryKey).toEqualTypeOf<['1']>()
return Number(context.queryKey[0]) + 42
}
useQuery(() => ({
queryKey: ['1'] as ['1'],
queryFn: getMyDataStringKey,
}))
// it should handle query-functions that return Promise<any>
useQuery(() => ({
queryKey: key,
queryFn: () => fetch('return Promise<any>').then((resp) => resp.json()),
}))
// handles wrapped queries with custom fetcher passed as inline queryFn
const useWrappedQuery = <
TQueryKey extends [string, Record<string, unknown>?],
TQueryFnData,
TError,
TData = TQueryFnData,
>(
qk: TQueryKey,
fetcher: (
obj: TQueryKey[1],
token: string,
// return type must be wrapped with TQueryFnReturn
) => Promise<TQueryFnData>,
options?: OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey' | 'queryFn' | 'initialData',
'safely'
>,
) =>
useQuery(() => ({
queryKey: qk,
queryFn: () => fetcher(qk[1], 'token'),
...options,
}))
const test = useWrappedQuery([''], () => Promise.resolve('1'))
expectTypeOf(test.data).toEqualTypeOf<string | undefined>()
// handles wrapped queries with custom fetcher passed directly to useQuery
const useWrappedFuncStyleQuery = <
TQueryKey extends [string, Record<string, unknown>?],
TQueryFnData,
TError,
TData = TQueryFnData,
>(
qk: TQueryKey,
fetcher: () => Promise<TQueryFnData>,
options?: OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey' | 'queryFn' | 'initialData',
'safely'
>,
) => useQuery(() => ({ queryKey: qk, queryFn: fetcher, ...options }))
const testFuncStyle = useWrappedFuncStyleQuery([''], () =>
Promise.resolve(true),
)
expectTypeOf(testFuncStyle.data).toEqualTypeOf<boolean | undefined>()
it('should return the correct states for a successful query', () => {
const state = useQuery<string, Error>(() => ({
queryKey: key,
queryFn: () => Promise.resolve('test'),
}))
if (state.isPending) {
expectTypeOf(state.data).toEqualTypeOf<undefined>()
expectTypeOf(state.error).toEqualTypeOf<null>()
} else if (state.isLoadingError) {
expectTypeOf(state.data).toEqualTypeOf<undefined>()
expectTypeOf(state.error).toEqualTypeOf<Error>()
} else {
expectTypeOf(state.data).toEqualTypeOf<string>()
expectTypeOf(state.error).toEqualTypeOf<Error | null>()
}
})
describe('initialData', () => {
describe('Config object overload', () => {
it('TData should always be defined when initialData is provided as an object', () => {
const { data } = useQuery(() => ({
queryKey: queryKey(),
queryFn: () => ({ wow: true }),
initialData: { wow: true },
}))
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
})
it('TData should be defined when passed through queryOptions', () => {
const options = queryOptions({
queryKey: queryKey(),
queryFn: () => ({ wow: true }),
initialData: { wow: true },
})
const { data } = useQuery(() => options)
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
})
it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => {
const { data } = useQuery(() => ({
queryKey: queryKey(),
queryFn: () => ({ wow: true }),
initialData: () => ({ wow: true }),
}))
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
})
it('TData should have undefined in the union when initialData is NOT provided', () => {
const { data } = useQuery(() => ({
queryKey: queryKey(),
queryFn: () => ({ wow: true }),
}))
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
})
it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
const { data } = useQuery(() => ({
queryKey: queryKey(),
queryFn: () => ({ wow: true }),
initialData: () => undefined as { wow: boolean } | undefined,
}))
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
})
})
describe('Query key overload', () => {
it('TData should always be defined when initialData is provided', () => {
const { data } = useQuery(() => ({
queryKey: queryKey(),
queryFn: () => ({ wow: true }),
initialData: { wow: true },
}))
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
})
it('TData should have undefined in the union when initialData is NOT provided', () => {
const { data } = useQuery(() => ({
queryKey: queryKey(),
queryFn: () => ({ wow: true }),
}))
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
})
})
describe('Query key and func', () => {
it('TData should always be defined when initialData is provided', () => {
const { data } = useQuery(() => ({
queryKey: queryKey(),
queryFn: () => ({ wow: true }),
initialData: { wow: true },
}))
expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>()
})
it('TData should have undefined in the union when initialData is NOT provided', () => {
const { data } = useQuery(() => ({
queryKey: queryKey(),
queryFn: () => ({ wow: true }),
}))
expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>()
})
})
})
describe('generic indexed access TData', () => {
// https://github.com/TanStack/query/issues/9937
it('should be assignable back to its source indexed type when passed to a generic function parameter', () => {
enum DataType {
Account = 'account',
Product = 'product',
}
interface Account {
name: string
}
interface Product {
code: string
}
type DataTypeToEntity = {
[DataType.Account]: Account
[DataType.Product]: Product
}
const getData = <TDataType extends DataType>(
_dataType: TDataType,
): Promise<DataTypeToEntity[TDataType]> =>
Promise.resolve({} as DataTypeToEntity[TDataType])
const getLabel = <TDataType extends DataType>(
_dataType: TDataType,
_data: DataTypeToEntity[TDataType],
) => 'test'
function Test<TDataType extends DataType>(props: {
dataType: TDataType
}) {
const { data } = useQuery(() => ({
queryKey: ['test'],
queryFn: () => getData(props.dataType),
}))
// Regression guard: this call must compile. With the previous
// hand-rolled NoInfer, `data` failed to flow back into the generic
// indexed-access parameter `DataTypeToEntity[TDataType]`.
return data ? getLabel(props.dataType, data) : null
}
expectTypeOf(Test).toBeFunction()
})
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
import { vi } from 'vitest'
import { Show, createEffect, createSignal, onCleanup } from 'solid-js'
import { onlineManager } from '@tanstack/query-core'
import { render } from '@solidjs/testing-library'
import { QueryClientProvider } from '..'
import type { JSX, ParentProps } from 'solid-js'
import type { MockInstance } from 'vitest'
import type { QueryClient } from '..'
export function renderWithClient(
client: QueryClient,
ui: () => JSX.Element,
): ReturnType<typeof render> {
return render(() => (
<QueryClientProvider client={client}>{ui()}</QueryClientProvider>
))
}
export function Blink(
props: {
duration: number
} & ParentProps,
) {
const [shouldShow, setShouldShow] = createSignal<boolean>(true)
createEffect(() => {
setShouldShow(true)
const timeout = setActTimeout(() => setShouldShow(false), props.duration)
onCleanup(() => clearTimeout(timeout))
})
return (
<Show when={shouldShow()} fallback={<>off</>}>
<>{props.children}</>
</Show>
)
}
export function mockOnlineManagerIsOnline(
value: boolean,
): MockInstance<() => boolean> {
return vi.spyOn(onlineManager, 'isOnline').mockReturnValue(value)
}
export function setActTimeout(fn: () => void, ms?: number) {
return setTimeout(() => {
fn()
}, ms)
}