import { describe, expect, expectTypeOf, it, vi } from 'vitest' import { fireEvent, render, waitFor } from '@testing-library/react' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { QueryClient } from '@tanstack/query-core' import { QueryCache, queryOptions, skipToken, useQueries } from '..' import { createQueryClient, queryKey, renderWithClient, sleep } from './utils' import type { QueryFunction, QueryKey, QueryObserverResult, UseQueryOptions, UseQueryResult, } from '..' import type { QueryFunctionContext } from '@tanstack/query-core' describe('useQueries', () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) it('should return the correct states', async () => { const key1 = queryKey() const key2 = queryKey() const results: Array> = [] function Page() { const result = useQueries({ queries: [ { queryKey: key1, queryFn: async () => { await sleep(10) return 1 }, }, { queryKey: key2, queryFn: async () => { await sleep(200) return 2 }, }, ], }) results.push(result) return (
data1: {String(result[0].data ?? 'null')}, data2:{' '} {String(result[1].data ?? 'null')}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data1: 1, data2: 2')) 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 }]) }) it('should track results', async () => { const key1 = queryKey() const results: Array> = [] let count = 0 function Page() { const result = useQueries({ queries: [ { queryKey: key1, queryFn: async () => { await sleep(10) count++ return count }, }, ], }) results.push(result) return (
data: {String(result[0].data ?? 'null')}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: 1')) expect(results.length).toBe(2) expect(results[0]).toMatchObject([{ data: undefined }]) expect(results[1]).toMatchObject([{ data: 1 }]) fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await waitFor(() => rendered.getByText('data: 2')) // only one render for data update, no render for isFetching transition expect(results.length).toBe(3) expect(results[2]).toMatchObject([{ data: 2 }]) }) it('handles type parameter - tuple of tuples', async () => { 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, boolean]] >({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], }, ], }) expectTypeOf(result1[0]).toEqualTypeOf>() expectTypeOf(result1[1]).toEqualTypeOf>() expectTypeOf(result1[2]).toEqualTypeOf< UseQueryResult, boolean> >() expectTypeOf(result1[0].data).toEqualTypeOf() expectTypeOf(result1[1].data).toEqualTypeOf() expectTypeOf(result1[2].data).toEqualTypeOf | undefined>() expectTypeOf(result1[2].error).toEqualTypeOf() // 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() return a.toLowerCase() }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, }, ], }) expectTypeOf(result2[0]).toEqualTypeOf>() expectTypeOf(result2[1]).toEqualTypeOf>() expectTypeOf(result2[0].data).toEqualTypeOf() expectTypeOf(result2[1].data).toEqualTypeOf() // types should be enforced useQueries<[[string, unknown, string], [string, boolean, number]]>({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, ], }) // field names should be enforced useQueries<[[string]]>({ queries: [ { queryKey: key1, queryFn: () => 'string', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) } }) it('handles type parameter - tuple of objects', async () => { 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; error: boolean }, ] >({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], }, ], }) expectTypeOf(result1[0]).toEqualTypeOf>() expectTypeOf(result1[1]).toEqualTypeOf>() expectTypeOf(result1[2]).toEqualTypeOf< UseQueryResult, boolean> >() expectTypeOf(result1[0].data).toEqualTypeOf() expectTypeOf(result1[1].data).toEqualTypeOf() expectTypeOf(result1[2].data).toEqualTypeOf | undefined>() expectTypeOf(result1[2].error).toEqualTypeOf() // 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() return a.toLowerCase() }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, }, ], }) expectTypeOf(result2[0]).toEqualTypeOf>() expectTypeOf(result2[1]).toEqualTypeOf>() expectTypeOf(result2[0].data).toEqualTypeOf() expectTypeOf(result2[1].data).toEqualTypeOf() // 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() return a as string }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a as number }, }, ], }) expectTypeOf(result3[0]).toEqualTypeOf>() expectTypeOf(result3[1]).toEqualTypeOf>() expectTypeOf(result3[0].data).toEqualTypeOf() expectTypeOf(result3[1].data).toEqualTypeOf() // 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() return a.toLowerCase() }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() 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', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) } }) it('correctly returns types when passing through queryOptions', () => { // @ts-expect-error (Page component is not rendered) function Page() { // data and results types are correct when using queryOptions const result4 = useQueries({ queries: [ queryOptions({ queryKey: ['key1'], queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, }), queryOptions({ queryKey: ['key2'], queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, }), ], }) expectTypeOf(result4[0]).toEqualTypeOf>() expectTypeOf(result4[1]).toEqualTypeOf>() expectTypeOf(result4[0].data).toEqualTypeOf() expectTypeOf(result4[1].data).toEqualTypeOf() } }) it('handles array literal without type parameter to infer result type', async () => { const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const key4 = queryKey() const key5 = queryKey() type BizError = { code: number } const throwOnError = (_error: BizError) => true // @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> >() if (result1[0]) { expectTypeOf(result1[0].data).toEqualTypeOf() } // Array.map preserves TError const result1_err = useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, throwOnError, })), }) expectTypeOf(result1_err).toEqualTypeOf< Array> >() if (result1_err[0]) { expectTypeOf(result1_err[0].data).toEqualTypeOf() expectTypeOf(result1_err[0].error).toEqualTypeOf() } // 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> >() const result2_err = useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), throwOnError, })), }) expectTypeOf(result2_err).toEqualTypeOf< Array> >() const result3 = useQueries({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], select: () => 123, }, { queryKey: key5, queryFn: () => 'string', throwOnError, }, ], }) expectTypeOf(result3[0]).toEqualTypeOf>() expectTypeOf(result3[1]).toEqualTypeOf>() expectTypeOf(result3[2]).toEqualTypeOf>() expectTypeOf(result3[0].data).toEqualTypeOf() expectTypeOf(result3[1].data).toEqualTypeOf() expectTypeOf(result3[3].data).toEqualTypeOf() // select takes precedence over queryFn expectTypeOf(result3[2].data).toEqualTypeOf() // infer TError from throwOnError expectTypeOf(result3[3].error).toEqualTypeOf() // 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 and throwOnError 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), }, { queryKey: key5, queryFn: () => 'string', throwOnError, }, ], }) // 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), }, { queryKey: key5, queryFn: () => 'string', select: (a: string) => parseInt(a), throwOnError, }, ], }) expectTypeOf(result4[0]).toEqualTypeOf>() expectTypeOf(result4[1]).toEqualTypeOf>() expectTypeOf(result4[2]).toEqualTypeOf>() expectTypeOf(result4[3]).toEqualTypeOf>() // handles when queryFn returns a Promise const result5 = useQueries({ queries: [ { queryKey: key1, queryFn: () => Promise.resolve('string'), }, ], }) expectTypeOf(result5[0]).toEqualTypeOf>() // Array as const does not throw error const result6 = useQueries({ queries: [ { queryKey: ['key1'], queryFn: () => 'string', }, { queryKey: ['key1'], queryFn: () => 123, }, { queryKey: key5, queryFn: () => 'string', throwOnError, }, ], } as const) expectTypeOf(result6[0]).toEqualTypeOf>() expectTypeOf(result6[1]).toEqualTypeOf>() expectTypeOf(result6[2]).toEqualTypeOf>() // field names should be enforced - array literal useQueries({ queries: [ { queryKey: key1, queryFn: () => 'string', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) // 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', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) // supports queryFn using fetch() to return Promise - Array.map() result useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => fetch('return Promise').then((resp) => resp.json()), })), }) // supports queryFn using fetch() to return Promise - array literal useQueries({ queries: [ { queryKey: key1, queryFn: () => fetch('return Promise').then((resp) => resp.json()), }, ], }) } }) it('handles strongly typed queryFn factories and useQueries wrappers', () => { // QueryKey + queryFn factory type QueryKeyA = ['queryA'] const getQueryKeyA = (): QueryKeyA => ['queryA'] type GetQueryFunctionA = () => QueryFunction const getQueryFunctionA: GetQueryFunctionA = () => async () => { 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 const getQueryFunctionB: GetQueryFunctionB = () => async () => { 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>) { return useQueries({ queries: queries.map( // no need to type the mapped query (query) => { const { queryFn: fn, queryKey: key } = query expectTypeOf(fn).toEqualTypeOf< | typeof skipToken | QueryFunction | undefined >() return { queryKey: key, queryFn: fn && fn !== skipToken ? (ctx: QueryFunctionContext) => { // eslint-disable-next-line vitest/valid-expect expectTypeOf(ctx.queryKey) return fn.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>() expectTypeOf(result[1]).toEqualTypeOf>() 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> >() } }) it("should throw error if in one of queries' queryFn throws and throwOnError is in use", async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const key4 = queryKey() function Page() { useQueries({ queries: [ { queryKey: key1, queryFn: () => Promise.reject( new Error( 'this should not throw because throwOnError is not set', ), ), }, { queryKey: key2, queryFn: () => Promise.reject(new Error('single query error')), throwOnError: true, retry: false, }, { queryKey: key3, queryFn: async () => 2, }, { queryKey: key4, queryFn: async () => Promise.reject( new Error('this should not throw because query#2 already did'), ), throwOnError: true, retry: false, }, ], }) return null } const rendered = renderWithClient( queryClient, (
error boundary
{error.message}
)} >
, ) await waitFor(() => rendered.getByText('error boundary')) await waitFor(() => rendered.getByText('single query error')) consoleMock.mockRestore() }) it("should throw error if in one of queries' queryFn throws and throwOnError function resolves to true", async () => { const consoleMock = vi .spyOn(console, 'error') .mockImplementation(() => undefined) const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const key4 = queryKey() function Page() { useQueries({ queries: [ { queryKey: key1, queryFn: () => Promise.reject( new Error( 'this should not throw because throwOnError function resolves to false', ), ), throwOnError: () => false, retry: false, }, { queryKey: key2, queryFn: async () => 2, }, { queryKey: key3, queryFn: () => Promise.reject(new Error('single query error')), throwOnError: () => true, retry: false, }, { queryKey: key4, queryFn: async () => Promise.reject( new Error('this should not throw because query#3 already did'), ), throwOnError: true, retry: false, }, ], }) return null } const rendered = renderWithClient( queryClient, (
error boundary
{error.message}
)} >
, ) await waitFor(() => rendered.getByText('error boundary')) await waitFor(() => rendered.getByText('single query error')) consoleMock.mockRestore() }) it('should use provided custom queryClient', async () => { const key = queryKey() const queryFn = async () => { return Promise.resolve('custom client') } function Page() { const queries = useQueries( { queries: [ { queryKey: key, queryFn, }, ], }, queryClient, ) return
data: {queries[0].data}
} const rendered = render() await waitFor(() => rendered.getByText('data: custom client')) }) it('should combine queries', async () => { const key1 = queryKey() const key2 = queryKey() function Page() { const queries = useQueries( { queries: [ { queryKey: key1, queryFn: () => Promise.resolve('first result'), }, { queryKey: key2, queryFn: () => Promise.resolve('second result'), }, ], combine: (results) => { return { combined: true, res: results.map((res) => res.data).join(','), } }, }, queryClient, ) return (
data: {String(queries.combined)} {queries.res}
) } const rendered = render() await waitFor(() => rendered.getByText('data: true first result,second result'), ) }) it('should not return new instances when called without queries', async () => { const key = queryKey() const ids: Array = [] let resultChanged = 0 function Page() { const [count, setCount] = React.useState(0) const result = useQueries({ queries: ids.map((id) => { return { queryKey: [key, id], queryFn: async () => async () => { return { id, content: { value: Math.random() }, } }, } }), combine: () => ({ empty: 'object' }), }) React.useEffect(() => { resultChanged++ }, [result]) return (
count: {count}
data: {JSON.stringify(result)}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: {"empty":"object"}')) await waitFor(() => rendered.getByText('count: 0')) expect(resultChanged).toBe(1) fireEvent.click(rendered.getByRole('button', { name: /inc/i })) await waitFor(() => rendered.getByText('count: 1')) // there should be no further effect calls because the returned object is structurally shared expect(resultChanged).toBe(1) }) it('should not have infinite render loops with empty queries (#6645)', async () => { let renderCount = 0 function Page() { const result = useQueries({ queries: [], }) React.useEffect(() => { renderCount++ }) return
data: {JSON.stringify(result)}
} renderWithClient(queryClient, ) await sleep(10) expect(renderCount).toBe(1) }) it('should only call combine with query results', async () => { const key1 = queryKey() const key2 = queryKey() function Page() { const result = useQueries({ queries: [ { queryKey: key1, queryFn: async () => { await sleep(5) return Promise.resolve('query1') }, }, { queryKey: key2, queryFn: async () => { await sleep(20) return Promise.resolve('query2') }, }, ], combine: ([query1, query2]) => { return { data: { query1: query1.data, query2: query2.data }, } }, }) return
data: {JSON.stringify(result)}
} const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText( 'data: {"data":{"query1":"query1","query2":"query2"}}', ), ) }) it('should track property access through combine function', async () => { const key1 = queryKey() const key2 = queryKey() let count = 0 const results: Array = [] function Page() { const queries = useQueries( { queries: [ { queryKey: key1, queryFn: async () => { await sleep(5) return Promise.resolve('first result ' + count) }, }, { queryKey: key2, queryFn: async () => { await sleep(50) return Promise.resolve('second result ' + count) }, }, ], combine: (queryResults) => { return { combined: true, refetch: () => queryResults.forEach((res) => res.refetch()), res: queryResults .flatMap((res) => (res.data ? [res.data] : [])) .join(','), } }, }, queryClient, ) results.push(queries) return (
data: {String(queries.combined)} {queries.res}
) } const rendered = render() await waitFor(() => rendered.getByText('data: true first result 0,second result 0'), ) expect(results.length).toBe(3) expect(results[0]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: '', }) expect(results[1]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: 'first result 0', }) expect(results[2]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: 'first result 0,second result 0', }) count++ fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await waitFor(() => rendered.getByText('data: true first result 1,second result 1'), ) const length = results.length expect([4, 5]).toContain(results.length) expect(results[results.length - 1]).toStrictEqual({ combined: true, refetch: expect.any(Function), res: 'first result 1,second result 1', }) fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await sleep(100) // no further re-render because data didn't change expect(results.length).toBe(length) }) it('should synchronously track properties of all observer even if a property (isLoading) is only accessed on one observer (#7000)', async () => { const key = queryKey() const ids = [1, 2] function Page() { const { isLoading } = useQueries({ queries: ids.map((id) => ({ queryKey: [key, id], queryFn: () => { return new Promise<{ id: number title: string }>((resolve, reject) => { if (id === 2) { setTimeout(() => { reject(new Error('FAILURE')) }, 10) } setTimeout(() => { resolve({ id, title: `Post ${id}` }) }, 10) }) }, retry: false, })), combine: (results) => { // this tracks data on all observers void results.forEach((result) => result.data) return { // .some aborts early, so `isLoading` might not be accessed (and thus tracked) on all observers // leading to missing re-renders isLoading: results.some((result) => result.isLoading), } }, }) return (

Loading Status: {isLoading ? 'Loading...' : 'Loaded'}

) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Loading Status: Loading...')) await waitFor(() => rendered.getByText('Loading Status: Loaded')) }) it('should not have stale closures with combine (#6648)', async () => { const key = queryKey() function Page() { const [count, setCount] = React.useState(0) const queries = useQueries( { queries: [ { queryKey: key, queryFn: () => Promise.resolve('result'), }, ], combine: (results) => { return { count, res: results.map((res) => res.data).join(','), } }, }, queryClient, ) return (
data: {String(queries.count)} {queries.res}
) } const rendered = render() await waitFor(() => rendered.getByText('data: 0 result')) fireEvent.click(rendered.getByRole('button', { name: /inc/i })) await waitFor(() => rendered.getByText('data: 1 result')) }) it('should optimize combine if it is a stable reference', async () => { const key1 = queryKey() const key2 = queryKey() const client = new QueryClient() const spy = vi.fn() let value = 0 function Page() { const [state, setState] = React.useState(0) const queries = useQueries( { queries: [ { queryKey: key1, queryFn: async () => { await sleep(10) return 'first result:' + value }, }, { queryKey: key2, queryFn: async () => { await sleep(20) return 'second result:' + value }, }, ], combine: React.useCallback((results: Array) => { const result = { combined: true, res: results.map((res) => res.data).join(','), } spy(result) return result }, []), }, client, ) return (
data: {String(queries.combined)} {queries.res}
) } const rendered = render() await waitFor(() => rendered.getByText('data: true first result:0,second result:0'), ) // both pending, one pending, both resolved expect(spy).toHaveBeenCalledTimes(3) await client.refetchQueries() // no increase because result hasn't changed expect(spy).toHaveBeenCalledTimes(3) fireEvent.click(rendered.getByRole('button', { name: /rerender/i })) // no increase because just a re-render expect(spy).toHaveBeenCalledTimes(3) value = 1 await client.refetchQueries() await waitFor(() => rendered.getByText('data: true first result:1,second result:1'), ) // two value changes = two re-renders expect(spy).toHaveBeenCalledTimes(5) }) it('should re-run combine if the functional reference changes', async () => { const key1 = queryKey() const key2 = queryKey() const client = new QueryClient() const spy = vi.fn() function Page() { const [state, setState] = React.useState(0) const queries = useQueries( { queries: [ { queryKey: [key1], queryFn: async () => { await sleep(10) return 'first result' }, }, { queryKey: [key2], queryFn: async () => { await sleep(20) return 'second result' }, }, ], combine: React.useCallback( (results: Array) => { const result = { combined: true, state, res: results.map((res) => res.data).join(','), } spy(result) return result }, [state], ), }, client, ) return (
data: {String(queries.state)} {queries.res}
) } const rendered = render() await waitFor(() => rendered.getByText('data: 0 first result,second result'), ) // both pending, one pending, both resolved expect(spy).toHaveBeenCalledTimes(3) fireEvent.click(rendered.getByRole('button', { name: /rerender/i })) // state changed, re-run combine expect(spy).toHaveBeenCalledTimes(4) }) it('should not re-render if combine returns a stable reference', async () => { const key1 = queryKey() const key2 = queryKey() const client = new QueryClient() const queryFns: Array = [] let renders = 0 function Page() { const data = useQueries( { queries: [ { queryKey: [key1], queryFn: async () => { await sleep(10) queryFns.push('first result') return 'first result' }, }, { queryKey: [key2], queryFn: async () => { await sleep(20) queryFns.push('second result') return 'second result' }, }, ], combine: () => 'foo', }, client, ) renders++ return (
data: {data}
) } const rendered = render() await waitFor(() => rendered.getByText('data: foo')) await waitFor(() => expect(queryFns).toEqual(['first result', 'second result']), ) expect(renders).toBe(1) }) it('should re-render once combine returns a different reference', async () => { const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const client = new QueryClient() let renders = 0 function Page() { const data = useQueries( { queries: [ { queryKey: [key1], queryFn: async () => { await sleep(10) return 'first result' }, }, { queryKey: [key2], queryFn: async () => { await sleep(15) return 'second result' }, }, { queryKey: [key3], queryFn: async () => { await sleep(20) return 'third result' }, }, ], combine: (results) => { const isPending = results.some((res) => res.isPending) return isPending ? 'pending' : 'foo' }, }, client, ) renders++ return (
data: {data}
) } const rendered = render() await waitFor(() => rendered.getByText('data: pending')) await waitFor(() => rendered.getByText('data: foo')) // one with pending, one with foo expect(renders).toBe(2) }) })