import { act, fireEvent, waitFor } from '@testing-library/react' import '@testing-library/jest-dom' import * as React from 'react' import { Blink, createQueryClient, expectType, mockLogger, mockNavigatorOnLine, mockVisibilityState, queryKey, renderWithClient, setActTimeout, sleep, } from './utils' import type { DefinedUseQueryResult, QueryFunction, QueryFunctionContext, UseQueryOptions, UseQueryResult, } from '..' import { QueryCache, useQuery } from '..' import { ErrorBoundary } from 'react-error-boundary' describe('useQuery', () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) it('should return the correct types', () => { const key = queryKey() // @ts-ignore // eslint-disable-next-line function Page() { // unspecified query function should default to unknown const noQueryFn = useQuery(key) expectType(noQueryFn.data) expectType(noQueryFn.error) // it should infer the result type from the query function const fromQueryFn = useQuery(key, () => 'test') expectType(fromQueryFn.data) expectType(fromQueryFn.error) // it should be possible to specify the result type const withResult = useQuery(key, () => 'test') expectType(withResult.data) expectType(withResult.error) // it should be possible to specify the error type const withError = useQuery(key, () => 'test') expectType(withError.data) expectType(withError.error) // it should provide the result type in the configuration useQuery([key], async () => true, { onSuccess: (data) => expectType(data), onSettled: (data) => expectType(data), }) // it should be possible to specify a union type as result type const unionTypeSync = useQuery( key, () => (Math.random() > 0.5 ? 'a' : 'b'), { onSuccess: (data) => expectType<'a' | 'b'>(data), }, ) expectType<'a' | 'b' | undefined>(unionTypeSync.data) const unionTypeAsync = useQuery<'a' | 'b'>( key, () => Promise.resolve(Math.random() > 0.5 ? 'a' : 'b'), { onSuccess: (data) => expectType<'a' | 'b'>(data), }, ) expectType<'a' | 'b' | undefined>(unionTypeAsync.data) // should error when the query function result does not match with the specified type // @ts-expect-error useQuery(key, () => 'test') // it should infer the result type from a generic query function function queryFn(): Promise { return Promise.resolve({} as T) } const fromGenericQueryFn = useQuery(key, () => queryFn()) expectType(fromGenericQueryFn.data) expectType(fromGenericQueryFn.error) const fromGenericOptionsQueryFn = useQuery({ queryKey: key, queryFn: () => queryFn(), }) expectType(fromGenericOptionsQueryFn.data) expectType(fromGenericOptionsQueryFn.error) type MyData = number type MyQueryKey = readonly ['my-data', number] const getMyDataArrayKey: QueryFunction = async ({ queryKey: [, n], }) => { return n + 42 } useQuery({ queryKey: ['my-data', 100], queryFn: getMyDataArrayKey, }) const getMyDataStringKey: QueryFunction = async ( context, ) => { expectType<['1']>(context.queryKey) return Number(context.queryKey[0]) + 42 } useQuery({ queryKey: ['1'], queryFn: getMyDataStringKey, }) // it should handle query-functions that return Promise useQuery(key, () => fetch('return Promise').then((resp) => resp.json()), ) // handles wrapped queries with custom fetcher passed as inline queryFn const useWrappedQuery = < TQueryKey extends [string, Record?], TQueryFnData, TError, TData = TQueryFnData, >( qk: TQueryKey, fetcher: ( obj: TQueryKey[1], token: string, // return type must be wrapped with TQueryFnReturn ) => Promise, options?: Omit< UseQueryOptions, 'queryKey' | 'queryFn' | 'initialData' >, ) => useQuery(qk, () => fetcher(qk[1], 'token'), options) const test = useWrappedQuery([''], async () => '1') expectType(test.data) // handles wrapped queries with custom fetcher passed directly to useQuery const useWrappedFuncStyleQuery = < TQueryKey extends [string, Record?], TQueryFnData, TError, TData = TQueryFnData, >( qk: TQueryKey, fetcher: () => Promise, options?: Omit< UseQueryOptions, 'queryKey' | 'queryFn' | 'initialData' >, ) => useQuery(qk, fetcher, options) const testFuncStyle = useWrappedFuncStyleQuery([''], async () => true) expectType(testFuncStyle.data) } }) // See https://github.com/tannerlinsley/react-query/issues/105 it('should allow to set default data value', async () => { const key = queryKey() function Page() { const { data = 'default' } = useQuery(key, async () => { await sleep(10) return 'test' }) return (

{data}

) } const rendered = renderWithClient(queryClient, ) rendered.getByText('default') await waitFor(() => rendered.getByText('test')) }) it('should return the correct states for a successful query', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const state = useQuery(key, async () => { await sleep(10) return 'test' }) states.push(state) if (state.isLoading) { expectType(state.data) expectType(state.error) return loading } if (state.isLoadingError) { expectType(state.data) expectType(state.error) return {state.error.message} } expectType(state.data) expectType(state.error) return {state.data} } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('test')) expect(states.length).toEqual(2) expect(states[0]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isLoading: true, isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, refetch: expect.any(Function), remove: expect.any(Function), status: 'loading', fetchStatus: 'fetching', }) expect(states[1]).toEqual({ data: 'test', dataUpdatedAt: expect.any(Number), error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isError: false, isFetched: true, isFetchedAfterMount: true, isFetching: false, isPaused: false, isLoading: false, isInitialLoading: false, isLoadingError: false, isPlaceholderData: false, isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: true, refetch: expect.any(Function), remove: expect.any(Function), status: 'success', fetchStatus: 'idle', }) }) it('should return the correct states for an unsuccessful query', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const state = useQuery( key, () => Promise.reject('rejected'), { retry: 1, retryDelay: 1, }, ) states.push(state) return (

Status: {state.status}

Failure Count: {state.failureCount}
Failure Reason: {state.failureReason}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Status: error')) expect(states[0]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isLoading: true, isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, refetch: expect.any(Function), remove: expect.any(Function), status: 'loading', fetchStatus: 'fetching', }) expect(states[1]).toEqual({ data: undefined, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, failureCount: 1, failureReason: 'rejected', errorUpdateCount: 0, isError: false, isFetched: false, isFetchedAfterMount: false, isFetching: true, isPaused: false, isLoading: true, isInitialLoading: true, isLoadingError: false, isPlaceholderData: false, isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, refetch: expect.any(Function), remove: expect.any(Function), status: 'loading', fetchStatus: 'fetching', }) expect(states[2]).toEqual({ data: undefined, dataUpdatedAt: 0, error: 'rejected', errorUpdatedAt: expect.any(Number), failureCount: 2, failureReason: 'rejected', errorUpdateCount: 1, isError: true, isFetched: true, isFetchedAfterMount: true, isFetching: false, isPaused: false, isLoading: false, isInitialLoading: false, isLoadingError: true, isPlaceholderData: false, isPreviousData: false, isRefetchError: false, isRefetching: false, isStale: true, isSuccess: false, refetch: expect.any(Function), remove: expect.any(Function), status: 'error', fetchStatus: 'idle', }) }) it('should set isFetchedAfterMount to true after a query has been fetched', async () => { const key = queryKey() await queryClient.prefetchQuery(key, () => 'prefetched') function Page() { const result = useQuery(key, () => 'new data') return ( <>
data: {result.data}
isFetched: {result.isFetched ? 'true' : 'false'}
isFetchedAfterMount: {result.isFetchedAfterMount ? 'true' : 'false'}
) } const rendered = renderWithClient(queryClient, ) rendered.getByText('data: prefetched') rendered.getByText('isFetched: true') rendered.getByText('isFetchedAfterMount: false') await waitFor(() => { rendered.getByText('data: new data') rendered.getByText('isFetched: true') rendered.getByText('isFetchedAfterMount: true') }) }) it('should call onSuccess after a query has been fetched', async () => { const key = queryKey() const states: UseQueryResult[] = [] const onSuccess = jest.fn() function Page() { const state = useQuery( key, async () => { await sleep(10) return 'data' }, { onSuccess }, ) states.push(state) return
data: {state.data}
} const rendered = renderWithClient(queryClient, ) await rendered.findByText('data: data') expect(states.length).toBe(2) expect(onSuccess).toHaveBeenCalledTimes(1) expect(onSuccess).toHaveBeenCalledWith('data') }) it('should call onSuccess after a query has been refetched', async () => { const key = queryKey() const states: UseQueryResult[] = [] const onSuccess = jest.fn() let count = 0 function Page() { const state = useQuery( key, async () => { count++ await sleep(10) return 'data' + count }, { onSuccess }, ) states.push(state) return (
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await rendered.findByText('data: data1') fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await rendered.findByText('data: data2') expect(states.length).toBe(3) //loading, success, success after refetch expect(count).toBe(2) expect(onSuccess).toHaveBeenCalledTimes(2) }) it('should call onSuccess after a disabled query has been fetched', async () => { const key = queryKey() const states: UseQueryResult[] = [] const onSuccess = jest.fn() function Page() { const state = useQuery(key, () => 'data', { enabled: false, onSuccess }) states.push(state) return (
isSuccess: {state.isSuccess ? 'true' : 'false'}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => { rendered.getByText('isSuccess: false') }) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await waitFor(() => { rendered.getByText('isSuccess: true') }) expect(onSuccess).toHaveBeenCalledTimes(1) expect(onSuccess).toHaveBeenCalledWith('data') }) it('should not call onSuccess if a component has unmounted', async () => { const key = queryKey() const states: UseQueryResult[] = [] const onSuccess = jest.fn() function Page() { const [show, setShow] = React.useState(true) React.useEffect(() => { setShow(false) }, [setShow]) return show ? : null } function Component() { const state = useQuery( key, async () => { await sleep(10) return 'data' }, { onSuccess }, ) states.push(state) return null } renderWithClient(queryClient, ) await sleep(50) expect(states.length).toBe(1) expect(onSuccess).toHaveBeenCalledTimes(0) }) it('should call onError after a query has been fetched with an error', async () => { const key = queryKey() const states: UseQueryResult[] = [] const onError = jest.fn() function Page() { const state = useQuery(key, () => Promise.reject('error'), { retry: false, onError, }) states.push(state) return null } renderWithClient(queryClient, ) await sleep(10) expect(states.length).toBe(2) expect(onError).toHaveBeenCalledTimes(1) expect(onError).toHaveBeenCalledWith('error') }) it('should not call onError when receiving a CancelledError', async () => { const key = queryKey() const onError = jest.fn() function Page() { const { status, fetchStatus } = useQuery( key, async () => { await sleep(10) return 23 }, { onError, }, ) return ( status: {status}, fetchStatus: {fetchStatus} ) } const rendered = renderWithClient(queryClient, ) rendered.getByText('status: loading, fetchStatus: fetching') await queryClient.cancelQueries(key) // query cancellation will reset the query to it's initial state await waitFor(() => rendered.getByText('status: loading, fetchStatus: idle'), ) expect(onError).not.toHaveBeenCalled() }) it('should call onSettled after a query has been fetched', async () => { const key = queryKey() const states: UseQueryResult[] = [] const onSettled = jest.fn() function Page() { const state = useQuery(key, () => 'data', { onSettled }) states.push(state) return
data: {state.data}
} const rendered = renderWithClient(queryClient, ) await waitFor(() => { rendered.getByText('data: data') }) expect(states.length).toBe(2) expect(onSettled).toHaveBeenCalledTimes(1) expect(onSettled).toHaveBeenCalledWith('data', null) }) it('should call onSettled after a query has been fetched with an error', async () => { const key = queryKey() const states: UseQueryResult[] = [] const onSettled = jest.fn() function Page() { const state = useQuery(key, () => Promise.reject('error'), { retry: false, onSettled, }) states.push(state) return null } renderWithClient(queryClient, ) await sleep(10) expect(states.length).toBe(2) expect(onSettled).toHaveBeenCalledTimes(1) expect(onSettled).toHaveBeenCalledWith(undefined, 'error') }) it('should not cancel an ongoing fetch when refetch is called with cancelRefetch=false if we have data already', async () => { const key = queryKey() let fetchCount = 0 function Page() { const { refetch } = useQuery( key, async () => { fetchCount++ await sleep(10) return 'data' }, { enabled: false, initialData: 'initialData' }, ) React.useEffect(() => { setActTimeout(() => { refetch() }, 5) setActTimeout(() => { refetch({ cancelRefetch: false }) }, 5) }, [refetch]) return null } renderWithClient(queryClient, ) await sleep(20) // first refetch only, second refetch is ignored expect(fetchCount).toBe(1) }) it('should cancel an ongoing fetch when refetch is called (cancelRefetch=true) if we have data already', async () => { const key = queryKey() let fetchCount = 0 function Page() { const { refetch } = useQuery( key, async () => { fetchCount++ await sleep(10) return 'data' }, { enabled: false, initialData: 'initialData' }, ) React.useEffect(() => { setActTimeout(() => { refetch() }, 5) setActTimeout(() => { refetch() }, 5) }, [refetch]) return null } renderWithClient(queryClient, ) await sleep(20) // first refetch (gets cancelled) and second refetch expect(fetchCount).toBe(2) }) it('should not cancel an ongoing fetch when refetch is called (cancelRefetch=true) if we do not have data yet', async () => { const key = queryKey() let fetchCount = 0 function Page() { const { refetch } = useQuery( key, async () => { fetchCount++ await sleep(10) return 'data' }, { enabled: false }, ) React.useEffect(() => { setActTimeout(() => { refetch() }, 5) setActTimeout(() => { refetch() }, 5) }, [refetch]) return null } renderWithClient(queryClient, ) await sleep(20) // first refetch will not get cancelled, second one gets skipped expect(fetchCount).toBe(1) }) it('should be able to watch a query without providing a query function', async () => { const key = queryKey() const states: UseQueryResult[] = [] queryClient.setQueryDefaults(key, { queryFn: () => 'data' }) function Page() { const state = useQuery(key) states.push(state) return null } renderWithClient(queryClient, ) await sleep(10) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'data' }) }) it('should pick up a query when re-mounting with cacheTime 0', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const [toggle, setToggle] = React.useState(false) return (
{toggle ? ( ) : ( )}
) } function Component({ value }: { value: string }) { const state = useQuery( key, async () => { await sleep(10) return 'data: ' + value }, { cacheTime: 0, notifyOnChangeProps: 'all', }, ) states.push(state) return (
{state.data}
) } const rendered = renderWithClient(queryClient, ) await rendered.findByText('data: 1') fireEvent.click(rendered.getByRole('button', { name: /toggle/i })) await rendered.findByText('data: 2') expect(states.length).toBe(4) // First load expect(states[0]).toMatchObject({ isLoading: true, isSuccess: false, isFetching: true, }) // First success expect(states[1]).toMatchObject({ isLoading: false, isSuccess: true, isFetching: false, }) // Switch, goes to fetching expect(states[2]).toMatchObject({ isLoading: false, isSuccess: true, isFetching: true, }) // Second success expect(states[3]).toMatchObject({ isLoading: false, isSuccess: true, isFetching: false, }) }) it('should not get into an infinite loop when removing a query with cacheTime 0 and rerendering', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const [, rerender] = React.useState({}) const state = useQuery( key, async () => { await sleep(5) return 'data' }, { cacheTime: 0, notifyOnChangeProps: 'all', }, ) states.push(state) return ( <>
{state.data}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => { rendered.getByText('data') }) fireEvent.click(rendered.getByRole('button', { name: 'remove' })) await waitFor(() => { rendered.getByText('data') }) // required to make sure no additional renders are happening after data is successfully fetched for the second time await sleep(100) expect(states.length).toBe(5) // First load expect(states[0]).toMatchObject({ isLoading: true, isSuccess: false }) // First success expect(states[1]).toMatchObject({ isLoading: false, isSuccess: true }) // Remove expect(states[2]).toMatchObject({ isLoading: true, isSuccess: false }) // Hook state update expect(states[3]).toMatchObject({ isLoading: true, isSuccess: false }) // Second success expect(states[4]).toMatchObject({ isLoading: false, isSuccess: true }) }) it('should fetch when refetchOnMount is false and nothing has been fetched yet', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const state = useQuery(key, () => 'test', { refetchOnMount: false, }) states.push(state) return null } renderWithClient(queryClient, ) await sleep(10) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }) it('should not fetch when refetchOnMount is false and data has been fetched already', async () => { const key = queryKey() const states: UseQueryResult[] = [] queryClient.setQueryData(key, 'prefetched') function Page() { const state = useQuery(key, () => 'test', { refetchOnMount: false, }) states.push(state) return null } renderWithClient(queryClient, ) await sleep(10) expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: 'prefetched' }) }) it('should be able to select a part of the data with select', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const state = useQuery(key, () => ({ name: 'test' }), { select: (data) => data.name, }) states.push(state) return
{state.data}
} const rendered = renderWithClient(queryClient, ) await waitFor(() => { rendered.getByText('test') }) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }) it('should be able to select a part of the data with select in object syntax', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const state = useQuery({ queryKey: key, queryFn: () => ({ name: 'test' }), select: (data) => data.name, }) states.push(state) return
{state.data}
} const rendered = renderWithClient(queryClient, ) await waitFor(() => { rendered.getByText('test') }) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }) it('should not re-render when it should only re-render only data change and the selected data did not change', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const state = useQuery(key, () => ({ name: 'test' }), { select: (data) => data.name, notifyOnChangeProps: ['data'], }) states.push(state) return (
{state.data}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => { rendered.getByText('test') }) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await waitFor(() => { rendered.getByText('test') }) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) // make sure no additional renders happen await sleep(50) expect(states.length).toBe(2) }) it('should throw an error when a selector throws', async () => { const key = queryKey() const states: UseQueryResult[] = [] const error = new Error('Select Error') function Page() { const state = useQuery(key, () => ({ name: 'test' }), { select: () => { throw error }, }) states.push(state) return
{state.status}
} const rendered = renderWithClient(queryClient, ) await waitFor(() => { rendered.getByText('error') }) expect(mockLogger.error).toHaveBeenCalledWith(error) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ status: 'loading', data: undefined }) expect(states[1]).toMatchObject({ status: 'error', error }) }) it('should not re-run a stable select when it re-renders if selector throws an error', async () => { const key = queryKey() const error = new Error('Select Error') let runs = 0 function Page() { const [, rerender] = React.useReducer(() => ({}), {}) const state = useQuery( key, () => (runs === 0 ? 'test' : 'test2'), { select: React.useCallback(() => { runs++ throw error }, []), }, ) return (
error: {state.error?.message}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('error: Select Error')) expect(runs).toEqual(1) fireEvent.click(rendered.getByRole('button', { name: 'rerender' })) await sleep(10) expect(runs).toEqual(1) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await sleep(10) expect(runs).toEqual(2) }) it('should track properties and only re-render when a tracked property changes', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const state = useQuery(key, async () => { await sleep(10) return 'test' }) states.push(state) const { refetch, data } = state React.useEffect(() => { setActTimeout(() => { if (data) { refetch() } }, 20) }, [refetch, data]) return (

{data ?? null}

) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('test')) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }) it('should always re-render if we are tracking props but not using any', async () => { const key = queryKey() let renderCount = 0 const states: UseQueryResult[] = [] function Page() { const state = useQuery(key, () => 'test') states.push(state) React.useEffect(() => { renderCount++ }, [state]) return (

hello

) } renderWithClient(queryClient, ) await waitFor(() => { expect(renderCount).toBe(2) }) // give it a bit more time to make sure no additional renders are triggered await sleep(20) expect(renderCount).toBe(2) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined }) expect(states[1]).toMatchObject({ data: 'test' }) }) it('should be able to remove a query', async () => { const key = queryKey() const states: UseQueryResult[] = [] let count = 0 function Page() { const [, rerender] = React.useState({}) const state = useQuery(key, () => ++count, { notifyOnChangeProps: 'all' }) states.push(state) const { remove } = state return (
data: {state.data ?? 'null'}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: 1')) fireEvent.click(rendered.getByRole('button', { name: /remove/i })) await sleep(20) fireEvent.click(rendered.getByRole('button', { name: /rerender/i })) await waitFor(() => rendered.getByText('data: 2')) expect(states.length).toBe(4) // Initial expect(states[0]).toMatchObject({ status: 'loading', data: undefined }) // Fetched expect(states[1]).toMatchObject({ status: 'success', data: 1 }) // Remove + Hook state update, batched expect(states[2]).toMatchObject({ status: 'loading', data: undefined }) // Fetched expect(states[3]).toMatchObject({ status: 'success', data: 2 }) }) it('should create a new query when refetching a removed query', async () => { const key = queryKey() const states: UseQueryResult[] = [] let count = 0 function Page() { const state = useQuery( key, async () => { await sleep(10) return ++count }, { notifyOnChangeProps: 'all' }, ) states.push(state) const { remove, refetch } = state return (
data: {state.data ?? 'null'}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: 1')) fireEvent.click(rendered.getByRole('button', { name: /remove/i })) await sleep(50) fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await waitFor(() => rendered.getByText('data: 2')) expect(states.length).toBe(4) // Initial expect(states[0]).toMatchObject({ data: undefined, dataUpdatedAt: 0 }) // Fetched expect(states[1]).toMatchObject({ data: 1 }) // Switch expect(states[2]).toMatchObject({ data: undefined, dataUpdatedAt: 0 }) // Fetched expect(states[3]).toMatchObject({ data: 2 }) }) it('should share equal data structures between query results', async () => { const key = queryKey() const result1 = [ { id: '1', done: false }, { id: '2', done: false }, ] const result2 = [ { id: '1', done: false }, { id: '2', done: true }, ] const states: UseQueryResult[] = [] let count = 0 function Page() { const state = useQuery( key, async () => { await sleep(10) count++ return count === 1 ? result1 : result2 }, { notifyOnChangeProps: 'all' }, ) states.push(state) const { refetch } = state return (
data: {String(state.data?.[1]?.done)}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: false')) await sleep(20) fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await waitFor(() => rendered.getByText('data: true')) await waitFor(() => expect(states.length).toBe(4)) const todos = states[2]?.data const todo1 = todos?.[0] const todo2 = todos?.[1] const newTodos = states[3]?.data const newTodo1 = newTodos?.[0] const newTodo2 = newTodos?.[1] expect(todos).toEqual(result1) expect(newTodos).toEqual(result2) expect(newTodos).not.toBe(todos) expect(newTodo1).toBe(todo1) expect(newTodo2).not.toBe(todo2) return null }) it('should use query function from hook when the existing query does not have a query function', async () => { const key = queryKey() const results: DefinedUseQueryResult[] = [] queryClient.setQueryData(key, 'set') function Page() { const result = useQuery( key, async () => { await sleep(10) return 'fetched' }, { initialData: 'initial', staleTime: Infinity, }, ) results.push(result) return (
isFetching: {result.isFetching}
data: {result.data}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: set')) fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await waitFor(() => rendered.getByText('data: fetched')) await waitFor(() => expect(results.length).toBe(3)) expect(results[0]).toMatchObject({ data: 'set', isFetching: false }) expect(results[1]).toMatchObject({ data: 'set', isFetching: true }) expect(results[2]).toMatchObject({ data: 'fetched', isFetching: false }) }) it('should update query stale state and refetch when invalidated with invalidateQueries', async () => { const key = queryKey() const states: UseQueryResult[] = [] let count = 0 function Page() { const state = useQuery( key, async () => { await sleep(10) count++ return count }, { staleTime: Infinity, notifyOnChangeProps: 'all' }, ) states.push(state) return (
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: 1')) fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await waitFor(() => rendered.getByText('data: 2')) await waitFor(() => expect(states.length).toBe(4)) expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isRefetching: false, isSuccess: false, isStale: true, }) expect(states[1]).toMatchObject({ data: 1, isFetching: false, isRefetching: false, isSuccess: true, isStale: false, }) expect(states[2]).toMatchObject({ data: 1, isFetching: true, isRefetching: true, isSuccess: true, isStale: true, }) expect(states[3]).toMatchObject({ data: 2, isFetching: false, isRefetching: false, isSuccess: true, isStale: false, }) }) it('should not update disabled query when refetched with refetchQueries', async () => { const key = queryKey() const states: UseQueryResult[] = [] let count = 0 function Page() { const state = useQuery( key, async () => { await sleep(10) count++ return count }, { enabled: false }, ) states.push(state) React.useEffect(() => { setActTimeout(() => { queryClient.refetchQueries({ queryKey: key }) }, 20) }, []) return null } renderWithClient(queryClient, ) await sleep(50) expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, isStale: true, }) }) it('should not refetch disabled query when invalidated with invalidateQueries', async () => { const key = queryKey() const states: UseQueryResult[] = [] let count = 0 function Page() { const state = useQuery( key, async () => { await sleep(10) count++ return count }, { enabled: false }, ) states.push(state) React.useEffect(() => { setActTimeout(() => { queryClient.invalidateQueries(key) }, 20) }, []) return null } renderWithClient(queryClient, ) await sleep(100) expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, isStale: true, }) }) it('should not fetch when switching to a disabled query', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery( [key, count], async () => { await sleep(5) return count }, { enabled: count === 0 }, ) states.push(state) return (
data: {state.data ?? 'undefined'}
count: {count}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: 0')) fireEvent.click(rendered.getByRole('button', { name: /increment/i })) await waitFor(() => { rendered.getByText('count: 1') rendered.getByText('data: undefined') }) // making sure no additional fetches are triggered await sleep(50) expect(states.length).toBe(3) // Fetch query expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, }) // Fetched query expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, }) // Switch to disabled query expect(states[2]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, }) }) it('should keep the previous data when keepPreviousData is set', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery( [key, count], async () => { await sleep(10) return count }, { keepPreviousData: true }, ) states.push(state) return (
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: 0')) fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await waitFor(() => rendered.getByText('data: 1')) // Initial expect(states[0]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, isPreviousData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPreviousData: false, }) // Set state expect(states[2]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPreviousData: true, }) // New data expect(states[3]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPreviousData: false, }) }) it('should transition to error state when keepPreviousData is set', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page({ count }: { count: number }) { const state = useQuery( [key, count], async () => { await sleep(10) if (count === 2) { throw new Error('Error test') } return Promise.resolve(count) }, { retry: false, keepPreviousData: true, }, ) states.push(state) return (

data: {state.data}

error: {state.error?.message}

previous data: {state.isPreviousData}

) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: 0')) act(() => rendered.rerender()) await waitFor(() => rendered.getByText('data: 1')) act(() => rendered.rerender()) await waitFor(() => rendered.getByText('error: Error test')) await waitFor(() => expect(states.length).toBe(8)) // Initial expect(states[0]).toMatchObject({ data: undefined, isFetching: true, status: 'loading', error: null, isPreviousData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, status: 'success', error: null, isPreviousData: false, }) // rerender Page 1 expect(states[2]).toMatchObject({ data: 0, isFetching: true, status: 'success', error: null, isPreviousData: true, }) // Hook state update expect(states[3]).toMatchObject({ data: 0, isFetching: true, status: 'success', error: null, isPreviousData: true, }) // New data expect(states[4]).toMatchObject({ data: 1, isFetching: false, status: 'success', error: null, isPreviousData: false, }) // rerender Page 2 expect(states[5]).toMatchObject({ data: 1, isFetching: true, status: 'success', error: null, isPreviousData: true, }) // Hook state update again expect(states[6]).toMatchObject({ data: 1, isFetching: true, status: 'success', error: null, isPreviousData: true, }) // Error expect(states[7]).toMatchObject({ data: undefined, isFetching: false, status: 'error', isPreviousData: false, }) expect(states[7]?.error).toHaveProperty('message', 'Error test') }) it('should not show initial data from next query if keepPreviousData is set', async () => { const key = queryKey() const states: DefinedUseQueryResult[] = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery( [key, count], async () => { await sleep(10) return count }, { initialData: 99, keepPreviousData: true }, ) states.push(state) return (

data: {state.data}, count: {count}, isFetching:{' '} {String(state.isFetching)}

) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: 0, count: 0, isFetching: false'), ) fireEvent.click(rendered.getByRole('button', { name: 'inc' })) await waitFor(() => rendered.getByText('data: 1, count: 1, isFetching: false'), ) expect(states.length).toBe(5) // Initial expect(states[0]).toMatchObject({ data: 99, isFetching: true, isSuccess: true, isPreviousData: false, }) // Fetched expect(states[1]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPreviousData: false, }) // Set state expect(states[2]).toMatchObject({ data: 99, isFetching: true, isSuccess: true, isPreviousData: false, }) // Hook state update expect(states[3]).toMatchObject({ data: 99, isFetching: true, isSuccess: true, isPreviousData: false, }) // New data expect(states[4]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPreviousData: false, }) }) it('should keep the previous data on disabled query when keepPreviousData is set', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery( [key, count], async () => { await sleep(10) return count }, { enabled: false, keepPreviousData: true, notifyOnChangeProps: 'all' }, ) states.push(state) return (
data: {state.data ?? 'undefined'}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => { rendered.getByText('data: undefined') }) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await waitFor(() => { rendered.getByText('data: 0') }) fireEvent.click(rendered.getByRole('button', { name: 'setCount' })) await waitFor(() => { rendered.getByText('data: 0') }) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await waitFor(() => { rendered.getByText('data: 1') }) // making sure no additional renders are triggered await sleep(20) expect(states.length).toBe(6) // Disabled query expect(states[0]).toMatchObject({ data: undefined, isFetching: false, isSuccess: false, isPreviousData: false, }) // Fetching query expect(states[1]).toMatchObject({ data: undefined, isFetching: true, isSuccess: false, isPreviousData: false, }) // Fetched query expect(states[2]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPreviousData: false, }) // Set state expect(states[3]).toMatchObject({ data: 0, isFetching: false, isSuccess: true, isPreviousData: true, }) // Fetching new query expect(states[4]).toMatchObject({ data: 0, isFetching: true, isSuccess: true, isPreviousData: true, }) // Fetched new query expect(states[5]).toMatchObject({ data: 1, isFetching: false, isSuccess: true, isPreviousData: false, }) }) it('should keep the previous data on disabled query when keepPreviousData is set and switching query key multiple times', async () => { const key = queryKey() const states: UseQueryResult[] = [] queryClient.setQueryData([key, 10], 10) await sleep(10) function Page() { const [count, setCount] = React.useState(10) const state = useQuery( [key, count], async () => { await sleep(10) return count }, { enabled: false, keepPreviousData: true, notifyOnChangeProps: 'all' }, ) states.push(state) const { refetch } = state React.useEffect(() => { setActTimeout(() => { setCount(11) }, 20) setActTimeout(() => { setCount(12) }, 30) setActTimeout(() => { refetch() }, 40) }, [refetch]) return null } renderWithClient(queryClient, ) await sleep(100) expect(states.length).toBe(5) // Disabled query expect(states[0]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, isPreviousData: false, }) // Set state expect(states[1]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, isPreviousData: true, }) // State update expect(states[2]).toMatchObject({ data: 10, isFetching: false, isSuccess: true, isPreviousData: true, }) // Refetch expect(states[3]).toMatchObject({ data: 10, isFetching: true, isSuccess: true, isPreviousData: true, }) // Refetch done expect(states[4]).toMatchObject({ data: 12, isFetching: false, isSuccess: true, isPreviousData: false, }) }) it('should use the correct query function when components use different configurations', async () => { const key = queryKey() const states: UseQueryResult[] = [] function FirstComponent() { const state = useQuery( key, async () => { await sleep(10) return 1 }, { notifyOnChangeProps: 'all' }, ) const refetch = state.refetch states.push(state) return (
data: {state.data}
) } function SecondComponent() { useQuery(key, () => 2, { notifyOnChangeProps: 'all' }) return null } function Page() { return ( <> ) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: 1')) fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await waitFor(() => expect(states.length).toBe(4)) expect(states[0]).toMatchObject({ data: undefined, }) expect(states[1]).toMatchObject({ data: 1, }) expect(states[2]).toMatchObject({ data: 1, }) // This state should be 1 instead of 2 expect(states[3]).toMatchObject({ data: 1, }) }) it('should be able to set different stale times for a query', async () => { const key = queryKey() const states1: UseQueryResult[] = [] const states2: UseQueryResult[] = [] await queryClient.prefetchQuery(key, async () => { await sleep(10) return 'prefetch' }) await sleep(20) function FirstComponent() { const state = useQuery( key, async () => { await sleep(10) return 'one' }, { staleTime: 100, }, ) states1.push(state) return null } function SecondComponent() { const state = useQuery( key, async () => { await sleep(10) return 'two' }, { staleTime: 10, }, ) states2.push(state) return null } function Page() { return ( <> ) } renderWithClient(queryClient, ) await sleep(200) expect(states1.length).toBe(4) expect(states2.length).toBe(3) expect(states1).toMatchObject([ // First render { data: 'prefetch', isStale: false, }, // Second useQuery started fetching { data: 'prefetch', isStale: false, }, // Second useQuery data came in { data: 'two', isStale: false, }, // Data became stale after 100ms { data: 'two', isStale: true, }, ]) expect(states2).toMatchObject([ // First render, data is stale and starts fetching { data: 'prefetch', isStale: true, }, // Second useQuery data came in { data: 'two', isStale: false, }, // Data became stale after 5ms { data: 'two', isStale: true, }, ]) }) it('should re-render when a query becomes stale', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const state = useQuery(key, () => 'test', { staleTime: 50, }) states.push(state) return null } renderWithClient(queryClient, ) await sleep(100) expect(states.length).toBe(3) expect(states[0]).toMatchObject({ isStale: true }) expect(states[1]).toMatchObject({ isStale: false }) expect(states[2]).toMatchObject({ isStale: true }) }) it('should not re-render when it should only re-render on data changes and the data did not change', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const state = useQuery( key, async () => { await sleep(5) return 'test' }, { notifyOnChangeProps: ['data'], }, ) states.push(state) return ( <>
{state.data}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => { rendered.getByText('test') }) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) // sleep is required to make sure no additional renders happen after click await sleep(20) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, status: 'loading', isFetching: true, }) expect(states[1]).toMatchObject({ data: 'test', status: 'success', isFetching: false, }) }) // See https://github.com/tannerlinsley/react-query/issues/137 it('should not override initial data in dependent queries', async () => { const key1 = queryKey() const key2 = queryKey() function Page() { const first = useQuery(key1, () => 'data', { enabled: false, initialData: 'init', }) const second = useQuery(key2, () => 'data', { enabled: false, initialData: 'init', }) return (

First Data: {first.data}

Second Data: {second.data}

First Status: {first.status}
Second Status: {second.status}
) } const rendered = renderWithClient(queryClient, ) rendered.getByText('First Data: init') rendered.getByText('Second Data: init') rendered.getByText('First Status: success') rendered.getByText('Second Status: success') }) it('should not override query configuration on render', async () => { const key = queryKey() const queryFn1 = async () => { await sleep(10) return 'data1' } const queryFn2 = async () => { await sleep(10) return 'data2' } function Page() { useQuery(key, queryFn1) useQuery(key, queryFn2) return null } renderWithClient(queryClient, ) expect(queryCache.find(key)!.options.queryFn).toBe(queryFn1) }) it('should batch re-renders', async () => { const key = queryKey() let renders = 0 const queryFn = async () => { await sleep(15) return 'data' } function Page() { const query1 = useQuery(key, queryFn) const query2 = useQuery(key, queryFn) renders++ return (
{query1.data} {query2.data}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => { rendered.getByText('data data') }) // Should be 2 instead of 3 expect(renders).toBe(2) }) it('should batch re-renders including hook callbacks', async () => { const key = queryKey() let renders = 0 let callbackCount = 0 const queryFn = async () => { await sleep(10) return 'data' } function Page() { const [count, setCount] = React.useState(0) useQuery(key, queryFn, { onSuccess: () => { setCount((x) => x + 1) }, }) useQuery(key, queryFn, { onSuccess: () => { setCount((x) => x + 1) }, }) React.useEffect(() => { renders++ callbackCount = count }) return
count: {count}
} const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('count: 2')) // Should be 2 / 3 instead of 5, uSES batches differently expect(renders).toBe(process.env.REACTJS_VERSION === '17' ? 2 : 3) // Both callbacks should have been executed expect(callbackCount).toBe(2) }) it('should render latest data even if react has discarded certain renders', async () => { const key = queryKey() function Page() { const [, setNewState] = React.useState('state') const state = useQuery(key, () => 'data') React.useEffect(() => { setActTimeout(() => { queryClient.setQueryData(key, 'new') // Update with same state to make react discard the next render setNewState('state') }, 10) }, []) return
{state.data}
} const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('new')) }) // See https://github.com/tannerlinsley/react-query/issues/170 it('should start with status loading, fetchStatus idle if enabled is false', async () => { const key1 = queryKey() const key2 = queryKey() function Page() { const first = useQuery(key1, () => 'data', { enabled: false, }) const second = useQuery(key2, () => 'data') return (
First Status: {first.status}, {first.fetchStatus}
Second Status: {second.status}, {second.fetchStatus}
) } const rendered = renderWithClient(queryClient, ) // use "act" to wait for state update and prevent console warning rendered.getByText('First Status: loading, idle') await waitFor(() => rendered.getByText('Second Status: loading, fetching')) await waitFor(() => rendered.getByText('Second Status: success, idle')) }) // See https://github.com/tannerlinsley/react-query/issues/144 it('should be in "loading" state by default', async () => { const key = queryKey() function Page() { const { status } = useQuery(key, async () => { await sleep(10) return 'test' }) return
status: {status}
} const rendered = renderWithClient(queryClient, ) rendered.getByText('status: loading') }) // See https://github.com/tannerlinsley/react-query/issues/147 it('should not pass stringified variables to query function', async () => { const key = queryKey() const variables = { number: 5, boolean: false, object: {}, array: [] } type CustomQueryKey = [typeof key, typeof variables] const states: UseQueryResult[] = [] const queryFn = async (ctx: QueryFunctionContext) => { await sleep(10) return ctx.queryKey } function Page() { const state = useQuery([key, variables], queryFn) states.push(state) return
{state.status}
} const rendered = renderWithClient(queryClient, ) await waitFor(() => { rendered.getByText('success') }) expect(states[1]?.data).toEqual([key, variables]) }) it('should not refetch query on focus when `enabled` is set to `false`', async () => { const key = queryKey() const queryFn = jest.fn().mockReturnValue('data') function Page() { const { data = 'default' } = useQuery(key, queryFn, { enabled: false, }) return (

{data}

) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('default')) act(() => { window.dispatchEvent(new FocusEvent('focus')) }) expect(queryFn).not.toHaveBeenCalled() }) it('should not refetch stale query on focus when `refetchOnWindowFocus` is set to `false`', async () => { const key = queryKey() const states: UseQueryResult[] = [] let count = 0 function Page() { const state = useQuery(key, () => count++, { staleTime: 0, refetchOnWindowFocus: false, }) states.push(state) return null } renderWithClient(queryClient, ) await sleep(10) act(() => { window.dispatchEvent(new FocusEvent('focus')) }) await sleep(10) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) }) it('should not refetch stale query on focus when `refetchOnWindowFocus` is set to a function that returns `false`', async () => { const key = queryKey() const states: UseQueryResult[] = [] let count = 0 function Page() { const state = useQuery(key, () => count++, { staleTime: 0, refetchOnWindowFocus: () => false, }) states.push(state) return null } renderWithClient(queryClient, ) await sleep(10) act(() => { window.dispatchEvent(new FocusEvent('focus')) }) await sleep(10) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) }) it('should not refetch fresh query on focus when `refetchOnWindowFocus` is set to `true`', async () => { const key = queryKey() const states: UseQueryResult[] = [] let count = 0 function Page() { const state = useQuery(key, () => count++, { staleTime: Infinity, refetchOnWindowFocus: true, }) states.push(state) return null } renderWithClient(queryClient, ) await sleep(10) act(() => { window.dispatchEvent(new FocusEvent('focus')) }) await sleep(10) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) }) it('should refetch fresh query on focus when `refetchOnWindowFocus` is set to `always`', async () => { const key = queryKey() const states: UseQueryResult[] = [] let count = 0 function Page() { const state = useQuery( key, async () => { await sleep(10) return count++ }, { staleTime: Infinity, refetchOnWindowFocus: 'always', }, ) states.push(state) return null } renderWithClient(queryClient, ) await sleep(20) act(() => { window.dispatchEvent(new FocusEvent('focus')) }) await sleep(20) await waitFor(() => expect(states.length).toBe(4)) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) expect(states[2]).toMatchObject({ data: 0, isFetching: true }) expect(states[3]).toMatchObject({ data: 1, isFetching: false }) }) it('should calculate focus behaviour for `refetchOnWindowFocus` depending on function', async () => { const key = queryKey() const states: UseQueryResult[] = [] let count = 0 function Page() { const state = useQuery( key, async () => { await sleep(10) return count++ }, { staleTime: 0, retry: 0, refetchOnWindowFocus: (query) => (query.state.data || 0) < 1, }, ) states.push(state) return
data: {String(state.data)}
} const rendered = renderWithClient(queryClient, ) await rendered.findByText('data: 0') expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isFetching: true }) expect(states[1]).toMatchObject({ data: 0, isFetching: false }) act(() => { window.dispatchEvent(new FocusEvent('focus')) }) await rendered.findByText('data: 1') // refetch should happen expect(states.length).toBe(4) expect(states[2]).toMatchObject({ data: 0, isFetching: true }) expect(states[3]).toMatchObject({ data: 1, isFetching: false }) act(() => { window.dispatchEvent(new FocusEvent('focus')) }) await sleep(20) // no more refetch now expect(states.length).toBe(4) }) it('should refetch fresh query when refetchOnMount is set to always', async () => { const key = queryKey() const states: UseQueryResult[] = [] await queryClient.prefetchQuery(key, () => 'prefetched') function Page() { const state = useQuery(key, () => 'data', { refetchOnMount: 'always', staleTime: Infinity, }) states.push(state) return null } renderWithClient(queryClient, ) await sleep(10) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'prefetched', isStale: false, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: false, isFetching: false, }) }) it('should refetch stale query when refetchOnMount is set to true', async () => { const key = queryKey() const states: UseQueryResult[] = [] await queryClient.prefetchQuery(key, () => 'prefetched') await sleep(10) function Page() { const state = useQuery(key, () => 'data', { refetchOnMount: true, staleTime: 0, }) states.push(state) return null } renderWithClient(queryClient, ) await sleep(10) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'prefetched', isStale: true, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: true, isFetching: false, }) }) it('should set status to error if queryFn throws', async () => { const key = queryKey() function Page() { const { status, error } = useQuery( key, () => { return Promise.reject('Error test jaylen') }, { retry: false }, ) return (

{status}

{error}

) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('error')) await waitFor(() => rendered.getByText('Error test jaylen')) }) it('should throw error if queryFn throws and useErrorBoundary is in use', async () => { const key = queryKey() function Page() { const { status, error } = useQuery( key, () => Promise.reject('Error test jaylen'), { retry: false, useErrorBoundary: true }, ) return (

{status}

{error}

) } const rendered = renderWithClient( queryClient,
error boundary
}>
, ) await waitFor(() => rendered.getByText('error boundary')) }) it('should update with data if we observe no properties and useErrorBoundary', async () => { const key = queryKey() let result: UseQueryResult | undefined function Page() { const query = useQuery(key, () => Promise.resolve('data'), { useErrorBoundary: true, }) React.useEffect(() => { result = query }) return null } renderWithClient(queryClient, ) await sleep(10) await waitFor(() => expect(queryClient.isFetching()).toBe(0)) expect(result?.data).toBe('data') }) it('should set status to error instead of throwing when error should not be thrown', async () => { const key = queryKey() function Page() { const { status, error } = useQuery( key, () => Promise.reject('Local Error'), { retry: false, useErrorBoundary: (err) => err !== 'Local Error', }, ) return (

{status}

{error}

) } const rendered = renderWithClient( queryClient,
error boundary
}>
, ) await waitFor(() => rendered.getByText('error')) await waitFor(() => rendered.getByText('Local Error')) }) it('should throw error instead of setting status when error should be thrown', async () => { const key = queryKey() function Page() { const { status, error } = useQuery( key, () => Promise.reject(new Error('Remote Error')), { retry: false, useErrorBoundary: (err) => err.message !== 'Local Error', }, ) return (

{status}

{error?.message ?? ''}

) } const rendered = renderWithClient( queryClient, (
error boundary
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
{error?.message}
)} >
, ) await waitFor(() => rendered.getByText('error boundary')) await waitFor(() => rendered.getByText('Remote Error')) }) it('should continue retries when observers unmount and remount while waiting for a retry (#3031)', async () => { const key = queryKey() let count = 0 function Page() { const result = useQuery( key, async () => { count++ await sleep(10) return Promise.reject('some error') }, { retry: 2, retryDelay: 100, }, ) return (
error: {result.error ?? 'null'}
failureCount: {result.failureCount}
failureReason: {result.failureReason}
) } function App() { const [show, toggle] = React.useReducer((x) => !x, true) return (
{show && }
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('failureCount: 1')) await waitFor(() => rendered.getByText('failureReason: some error')) fireEvent.click(rendered.getByRole('button', { name: /hide/i })) await waitFor(() => rendered.getByRole('button', { name: /show/i })) fireEvent.click(rendered.getByRole('button', { name: /show/i })) await waitFor(() => rendered.getByText('error: some error')) expect(count).toBe(3) }) it('should restart when observers unmount and remount while waiting for a retry when query was cancelled in between (#3031)', async () => { const key = queryKey() let count = 0 function Page() { const result = useQuery( key, async () => { count++ await sleep(10) return Promise.reject('some error') }, { retry: 2, retryDelay: 100, }, ) return (
error: {result.error ?? 'null'}
failureCount: {result.failureCount}
failureReason: {result.failureReason}
) } function App() { const [show, toggle] = React.useReducer((x) => !x, true) return (
{show && }
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('failureCount: 1')) await waitFor(() => rendered.getByText('failureReason: some error')) fireEvent.click(rendered.getByRole('button', { name: /hide/i })) fireEvent.click(rendered.getByRole('button', { name: /cancel/i })) await waitFor(() => rendered.getByRole('button', { name: /show/i })) fireEvent.click(rendered.getByRole('button', { name: /show/i })) await waitFor(() => rendered.getByText('error: some error')) // initial fetch (1), which will be cancelled, followed by new mount(2) + 2 retries = 4 expect(count).toBe(4) }) it('should always fetch if refetchOnMount is set to always', async () => { const key = queryKey() const states: UseQueryResult[] = [] await queryClient.prefetchQuery(key, () => 'prefetched') function Page() { const state = useQuery(key, () => 'data', { refetchOnMount: 'always', staleTime: 50, }) states.push(state) return (
data: {state.data ?? 'null'}
isFetching: {state.isFetching}
isStale: {state.isStale}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: data')) await waitFor(() => expect(states.length).toBe(3)) expect(states[0]).toMatchObject({ data: 'prefetched', isStale: false, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: false, isFetching: false, }) expect(states[2]).toMatchObject({ data: 'data', isStale: true, isFetching: false, }) }) it('should fetch if initial data is set', async () => { const key = queryKey() const states: DefinedUseQueryResult[] = [] function Page() { const state = useQuery(key, () => 'data', { initialData: 'initial', }) states.push(state) return null } renderWithClient(queryClient, ) await sleep(50) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'initial', isStale: true, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: true, isFetching: false, }) }) it('should not fetch if initial data is set with a stale time', async () => { const key = queryKey() const states: DefinedUseQueryResult[] = [] function Page() { const state = useQuery(key, () => 'data', { staleTime: 50, initialData: 'initial', }) states.push(state) return null } renderWithClient(queryClient, ) await sleep(100) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'initial', isStale: false, isFetching: false, }) expect(states[1]).toMatchObject({ data: 'initial', isStale: true, isFetching: false, }) }) it('should fetch if initial data updated at is older than stale time', async () => { const key = queryKey() const states: DefinedUseQueryResult[] = [] const oneSecondAgo = Date.now() - 1000 function Page() { const state = useQuery(key, () => 'data', { staleTime: 50, initialData: 'initial', initialDataUpdatedAt: oneSecondAgo, }) states.push(state) return null } renderWithClient(queryClient, ) await sleep(100) expect(states.length).toBe(3) expect(states[0]).toMatchObject({ data: 'initial', isStale: true, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: false, isFetching: false, }) expect(states[2]).toMatchObject({ data: 'data', isStale: true, isFetching: false, }) }) it('should fetch if "initial data updated at" is exactly 0', async () => { const key = queryKey() const states: DefinedUseQueryResult[] = [] function Page() { const state = useQuery(key, () => 'data', { staleTime: 10 * 1000, // 10 seconds initialData: 'initial', initialDataUpdatedAt: 0, }) states.push(state) return null } renderWithClient(queryClient, ) await sleep(100) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 'initial', isStale: true, isFetching: true, }) expect(states[1]).toMatchObject({ data: 'data', isStale: false, isFetching: false, }) }) it('should keep initial data when the query key changes', async () => { const key = queryKey() const states: DefinedUseQueryResult<{ count: number }>[] = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery([key, count], () => ({ count: 10 }), { staleTime: Infinity, initialData: () => ({ count }), }) states.push(state) React.useEffect(() => { setActTimeout(() => { setCount(1) }, 10) }, []) return null } renderWithClient(queryClient, ) await sleep(100) expect(states.length).toBe(2) // Initial expect(states[0]).toMatchObject({ data: { count: 0 } }) // Set state expect(states[1]).toMatchObject({ data: { count: 1 } }) }) it('should retry specified number of times', async () => { const key = queryKey() const queryFn = jest.fn() queryFn.mockImplementation(() => { return Promise.reject('Error test Barrett') }) function Page() { const { status, failureCount, failureReason } = useQuery( key, queryFn, { retry: 1, retryDelay: 1, }, ) return (

{status}

Failed {failureCount} times

Failed because {failureReason}

) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('loading')) await waitFor(() => rendered.getByText('error')) // query should fail `retry + 1` times, since first time isn't a "retry" await waitFor(() => rendered.getByText('Failed 2 times')) await waitFor(() => rendered.getByText('Failed because Error test Barrett')) expect(queryFn).toHaveBeenCalledTimes(2) }) it('should not retry if retry function `false`', async () => { const key = queryKey() const queryFn = jest.fn() queryFn.mockImplementationOnce(() => { return Promise.reject('Error test Tanner') }) queryFn.mockImplementation(() => { return Promise.reject('NoRetry') }) function Page() { const { status, failureCount, failureReason, error } = useQuery< unknown, string, [string] >(key, queryFn, { retryDelay: 1, retry: (_failureCount, err) => err !== 'NoRetry', }) return (

{status}

Failed {failureCount} times

Failed because {failureReason}

{error}

) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('loading')) await waitFor(() => rendered.getByText('error')) await waitFor(() => rendered.getByText('Failed 2 times')) await waitFor(() => rendered.getByText('Failed because NoRetry')) await waitFor(() => rendered.getByText('NoRetry')) expect(queryFn).toHaveBeenCalledTimes(2) }) it('should extract retryDelay from error', async () => { const key = queryKey() type DelayError = { delay: number } const queryFn = jest.fn() queryFn.mockImplementation(() => { return Promise.reject({ delay: 50 }) }) function Page() { const { status, failureCount, failureReason } = useQuery(key, queryFn, { retry: 1, retryDelay: (_, error: DelayError) => error.delay, }) return (

{status}

Failed {failureCount} times

Failed because DelayError: {failureReason?.delay}ms

) } const rendered = renderWithClient(queryClient, ) await sleep(10) expect(queryFn).toHaveBeenCalledTimes(1) await waitFor(() => rendered.getByText('Failed because DelayError: 50ms')) await waitFor(() => rendered.getByText('Failed 2 times')) expect(queryFn).toHaveBeenCalledTimes(2) }) // See https://github.com/tannerlinsley/react-query/issues/160 it('should continue retry after focus regain', async () => { const key = queryKey() // make page unfocused const visibilityMock = mockVisibilityState('hidden') let count = 0 function Page() { const query = useQuery( key, () => { count++ return Promise.reject(`fetching error ${count}`) }, { retry: 3, retryDelay: 1, }, ) return (
error {String(query.error)}
status {query.status}
failureCount {query.failureCount}
failureReason {query.failureReason}
) } const rendered = renderWithClient(queryClient, ) // The query should display the first error result await waitFor(() => rendered.getByText('failureCount 1')) await waitFor(() => rendered.getByText('failureReason fetching error 1')) await waitFor(() => rendered.getByText('status loading')) await waitFor(() => rendered.getByText('error null')) // Check if the query really paused await sleep(10) await waitFor(() => rendered.getByText('failureCount 1')) await waitFor(() => rendered.getByText('failureReason fetching error 1')) act(() => { // reset visibilityState to original value visibilityMock.mockRestore() window.dispatchEvent(new FocusEvent('focus')) }) // Wait for the final result await waitFor(() => rendered.getByText('failureCount 4')) await waitFor(() => rendered.getByText('failureReason fetching error 4')) await waitFor(() => rendered.getByText('status error')) await waitFor(() => rendered.getByText('error fetching error 4')) // Check if the query really stopped await sleep(10) await waitFor(() => rendered.getByText('failureCount 4')) await waitFor(() => rendered.getByText('failureReason fetching error 4')) // Check if the error has been logged in the console expect(mockLogger.error).toHaveBeenCalledWith('fetching error 4') }) it('should fetch on mount when a query was already created with setQueryData', async () => { const key = queryKey() const states: UseQueryResult[] = [] queryClient.setQueryData(key, 'prefetched') function Page() { const state = useQuery(key, () => 'data') states.push(state) return null } renderWithClient(queryClient, ) await sleep(10) expect(states.length).toBe(2) expect(states).toMatchObject([ { data: 'prefetched', isFetching: true, isStale: true, }, { data: 'data', isFetching: false, isStale: true, }, ]) }) it('should refetch after focus regain', async () => { const key = queryKey() const states: UseQueryResult[] = [] // make page unfocused const visibilityMock = mockVisibilityState('hidden') // set data in cache to check if the hook query fn is actually called queryClient.setQueryData(key, 'prefetched') function Page() { const state = useQuery(key, async () => { await sleep(10) return 'data' }) states.push(state) return (
{state.data}, {state.isStale}, {state.isFetching}
) } renderWithClient(queryClient, ) await waitFor(() => expect(states.length).toBe(2)) act(() => { // reset visibilityState to original value visibilityMock.mockRestore() window.dispatchEvent(new FocusEvent('focus')) }) await waitFor(() => expect(states.length).toBe(4)) expect(states).toMatchObject([ { data: 'prefetched', isFetching: true, isStale: true, }, { data: 'data', isFetching: false, isStale: true, }, { data: 'data', isFetching: true, isStale: true, }, { data: 'data', isFetching: false, isStale: true, }, ]) }) // See https://github.com/tannerlinsley/react-query/issues/195 it('should refetch if stale after a prefetch', async () => { const key = queryKey() const states: UseQueryResult[] = [] const queryFn = jest.fn() queryFn.mockImplementation(() => 'data') const prefetchQueryFn = jest.fn() prefetchQueryFn.mockImplementation(() => 'not yet...') await queryClient.prefetchQuery(key, prefetchQueryFn, { staleTime: 10, }) await sleep(11) function Page() { const state = useQuery(key, queryFn) states.push(state) return null } renderWithClient(queryClient, ) await waitFor(() => expect(states.length).toBe(2)) expect(prefetchQueryFn).toHaveBeenCalledTimes(1) expect(queryFn).toHaveBeenCalledTimes(1) }) it('should not refetch if not stale after a prefetch', async () => { const key = queryKey() const queryFn = jest.fn() queryFn.mockImplementation(() => 'data') const prefetchQueryFn = jest.fn, unknown[]>() prefetchQueryFn.mockImplementation(async () => { await sleep(10) return 'not yet...' }) await queryClient.prefetchQuery(key, prefetchQueryFn, { staleTime: 1000, }) await sleep(0) function Page() { useQuery(key, queryFn, { staleTime: 1000, }) return null } renderWithClient(queryClient, ) await sleep(0) expect(prefetchQueryFn).toHaveBeenCalledTimes(1) expect(queryFn).toHaveBeenCalledTimes(0) }) // See https://github.com/tannerlinsley/react-query/issues/190 it('should reset failureCount on successful fetch', async () => { const key = queryKey() function Page() { let counter = 0 const query = useQuery( key, async () => { if (counter < 2) { counter++ throw new Error('error') } else { return 'data' } }, { retryDelay: 10 }, ) return (
failureCount {query.failureCount}
failureReason {query.failureReason?.message ?? 'null'}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('failureCount 2')) await waitFor(() => rendered.getByText('failureReason error')) await waitFor(() => rendered.getByText('failureCount 0')) await waitFor(() => rendered.getByText('failureReason null')) }) // See https://github.com/tannerlinsley/react-query/issues/199 it('should use prefetched data for dependent query', async () => { const key = queryKey() let count = 0 function Page() { const [enabled, setEnabled] = React.useState(false) const [isPrefetched, setPrefetched] = React.useState(false) const query = useQuery( key, async () => { count++ await sleep(10) return count }, { enabled, }, ) React.useEffect(() => { async function prefetch() { await queryClient.prefetchQuery(key, () => Promise.resolve('prefetched data'), ) act(() => setPrefetched(true)) } prefetch() }, []) return (
{isPrefetched &&
isPrefetched
}
data: {query.data}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('isPrefetched')) fireEvent.click(rendered.getByText('setKey')) await waitFor(() => rendered.getByText('data: prefetched data')) await waitFor(() => rendered.getByText('data: 1')) expect(count).toBe(1) }) it('should support dependent queries via the enable config option', async () => { const key = queryKey() function Page() { const [shouldFetch, setShouldFetch] = React.useState(false) const query = useQuery(key, () => 'data', { enabled: shouldFetch, }) return (
FetchStatus: {query.fetchStatus}

Data: {query.data || 'no data'}

{query.isStale ? ( ) : null}
) } const rendered = renderWithClient(queryClient, ) rendered.getByText('FetchStatus: idle') rendered.getByText('Data: no data') fireEvent.click(rendered.getByText('fetch')) await waitFor(() => rendered.getByText('FetchStatus: fetching')) await waitFor(() => [ rendered.getByText('FetchStatus: idle'), rendered.getByText('Data: data'), ]) }) it('should mark query as fetching, when using initialData', async () => { const key = queryKey() const results: DefinedUseQueryResult[] = [] function Page() { const result = useQuery(key, () => 'serverData', { initialData: 'data' }) results.push(result) return null } renderWithClient(queryClient, ) await sleep(10) expect(results.length).toBe(2) expect(results[0]).toMatchObject({ data: 'data', isFetching: true }) expect(results[1]).toMatchObject({ data: 'serverData', isFetching: false }) }) it('should initialize state properly, when initialData is falsy', async () => { const key = queryKey() const results: DefinedUseQueryResult[] = [] function Page() { const result = useQuery(key, () => 1, { initialData: 0 }) results.push(result) return null } renderWithClient(queryClient, ) await sleep(10) expect(results.length).toBe(2) expect(results[0]).toMatchObject({ data: 0, isFetching: true }) expect(results[1]).toMatchObject({ data: 1, isFetching: false }) }) it('should show the correct data when switching keys with initialData, keepPreviousData & staleTime', async () => { const key = queryKey() const ALL_TODOS = [ { name: 'todo A', priority: 'high' }, { name: 'todo B', priority: 'medium' }, ] const initialTodos = ALL_TODOS function Page() { const [filter, setFilter] = React.useState('') const { data: todos } = useQuery( [...key, filter], async () => { return ALL_TODOS.filter((todo) => filter ? todo.priority === filter : true, ) }, { initialData() { return filter === '' ? initialTodos : undefined }, keepPreviousData: true, staleTime: 5000, }, ) return (
Current Todos, filter: {filter || 'all'}
    {(todos ?? []).map((todo) => (
  • {todo.name} - {todo.priority}
  • ))}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Current Todos, filter: all')) fireEvent.click(rendered.getByRole('button', { name: /high/i })) await waitFor(() => rendered.getByText('Current Todos, filter: high')) fireEvent.click(rendered.getByRole('button', { name: /all/i })) await waitFor(() => rendered.getByText('todo B - medium')) }) // // See https://github.com/tannerlinsley/react-query/issues/214 it('data should persist when enabled is changed to false', async () => { const key = queryKey() const results: DefinedUseQueryResult[] = [] function Page() { const [shouldFetch, setShouldFetch] = React.useState(true) const result = useQuery(key, () => 'fetched data', { enabled: shouldFetch, initialData: shouldFetch ? 'initial' : 'initial falsy', }) results.push(result) return (
{result.data}
{shouldFetch ? 'enabled' : 'disabled'}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => { rendered.getByText('fetched data') rendered.getByText('enabled') }) fireEvent.click(rendered.getByRole('button', { name: /enable/i })) await waitFor(() => { rendered.getByText('fetched data') rendered.getByText('disabled') }) expect(results.length).toBe(3) expect(results[0]).toMatchObject({ data: 'initial', isStale: true }) expect(results[1]).toMatchObject({ data: 'fetched data', isStale: true }) expect(results[2]).toMatchObject({ data: 'fetched data', isStale: true }) }) it('it should support enabled:false in query object syntax', async () => { const key = queryKey() const queryFn = jest.fn() queryFn.mockImplementation(() => 'data') function Page() { const { fetchStatus } = useQuery({ queryKey: key, queryFn, enabled: false, }) return
fetchStatus: {fetchStatus}
} const rendered = renderWithClient(queryClient, ) expect(queryFn).not.toHaveBeenCalled() expect(queryCache.find(key)).not.toBeUndefined() rendered.getByText('fetchStatus: idle') }) // See https://github.com/tannerlinsley/react-query/issues/360 test('should init to status:loading, fetchStatus:idle when enabled is false', async () => { const key = queryKey() function Page() { const query = useQuery(key, () => 'data', { enabled: false, }) return (
status: {query.status}, {query.fetchStatus}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('status: loading, idle')) }) test('should not schedule garbage collection, if cacheTimeout is set to `Infinity`', async () => { const key = queryKey() function Page() { const query = useQuery(key, () => 'fetched data', { cacheTime: Infinity, }) return
{query.data}
} const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('fetched data')) rendered.unmount() const query = queryCache.find(key) // @ts-expect-error expect(query!.cacheTimeout).toBe(undefined) }) it('should not cause memo churn when data does not change', async () => { const key = queryKey() const queryFn = jest.fn().mockReturnValue('data') const memoFn = jest.fn() function Page() { const result = useQuery(key, async () => { await sleep(10) return ( queryFn() || { data: { nested: true, }, } ) }) React.useMemo(() => { memoFn() return result.data }, [result.data]) return (
status {result.status}
isFetching {result.isFetching ? 'true' : 'false'}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('status loading')) await waitFor(() => rendered.getByText('status success')) fireEvent.click(rendered.getByText('refetch')) await waitFor(() => rendered.getByText('isFetching true')) await waitFor(() => rendered.getByText('isFetching false')) expect(queryFn).toHaveBeenCalledTimes(2) expect(memoFn).toHaveBeenCalledTimes(2) }) it('should update data upon interval changes', async () => { const key = queryKey() let count = 0 function Page() { const [int, setInt] = React.useState(200) const { data } = useQuery(key, () => count++, { refetchInterval: int, }) React.useEffect(() => { if (data === 2) { setInt(0) } }, [data]) return
count: {data}
} const rendered = renderWithClient(queryClient, ) // mount await waitFor(() => rendered.getByText('count: 0')) await waitFor(() => rendered.getByText('count: 1')) await waitFor(() => rendered.getByText('count: 2')) }) it('should refetch in an interval depending on function result', async () => { const key = queryKey() let count = 0 const states: UseQueryResult[] = [] function Page() { const queryInfo = useQuery( key, async () => { await sleep(10) return count++ }, { refetchInterval: (data = 0) => (data < 2 ? 10 : false), }, ) states.push(queryInfo) return (

count: {queryInfo.data}

status: {queryInfo.status}

data: {queryInfo.data}

refetch: {queryInfo.isRefetching}

) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('count: 2')) expect(states.length).toEqual(6) expect(states).toMatchObject([ { status: 'loading', isFetching: true, data: undefined, }, { status: 'success', isFetching: false, data: 0, }, { status: 'success', isFetching: true, data: 0, }, { status: 'success', isFetching: false, data: 1, }, { status: 'success', isFetching: true, data: 1, }, { status: 'success', isFetching: false, data: 2, }, ]) }) it('should not interval fetch with a refetchInterval of 0', async () => { const key = queryKey() const states: UseQueryResult[] = [] function Page() { const queryInfo = useQuery(key, () => 1, { refetchInterval: 0, }) states.push(queryInfo) return
count: {queryInfo.data}
} const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('count: 1')) await sleep(10) //extra sleep to make sure we're not re-fetching expect(states.length).toEqual(2) expect(states).toMatchObject([ { status: 'loading', isFetching: true, data: undefined, }, { status: 'success', isFetching: false, data: 1, }, ]) }) it('should accept an empty string as query key', async () => { function Page() { const result = useQuery([''], (ctx) => ctx.queryKey) return <>{JSON.stringify(result.data)} } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('')) }) it('should accept an object as query key', async () => { function Page() { const result = useQuery([{ a: 'a' }], (ctx) => ctx.queryKey) return <>{JSON.stringify(result.data)} } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('[{"a":"a"}]')) }) it('should refetch if any query instance becomes enabled', async () => { const key = queryKey() const queryFn = jest.fn().mockReturnValue('data') function Disabled() { useQuery(key, queryFn, { enabled: false }) return null } function Page() { const [enabled, setEnabled] = React.useState(false) const result = useQuery(key, queryFn, { enabled }) return ( <>
{result.data}
) } const rendered = renderWithClient(queryClient, ) expect(queryFn).toHaveBeenCalledTimes(0) fireEvent.click(rendered.getByText('enable')) await waitFor(() => rendered.getByText('data')) expect(queryFn).toHaveBeenCalledTimes(1) }) it('should use placeholder data while the query loads', async () => { const key1 = queryKey() const states: UseQueryResult[] = [] function Page() { const state = useQuery(key1, () => 'data', { placeholderData: 'placeholder', }) states.push(state) return (

Data: {state.data}

Status: {state.status}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Data: data')) expect(states).toMatchObject([ { isSuccess: true, isPlaceholderData: true, data: 'placeholder', }, { isSuccess: true, isPlaceholderData: false, data: 'data', }, ]) }) it('should use placeholder data even for disabled queries', async () => { const key1 = queryKey() const states: { state: UseQueryResult; count: number }[] = [] function Page() { const [count, setCount] = React.useState(0) const state = useQuery(key1, () => 'data', { placeholderData: 'placeholder', enabled: count === 0, }) states.push({ state, count }) React.useEffect(() => { setCount(1) }, []) return (

Data: {state.data}

Status: {state.status}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Data: data')) expect(states).toMatchObject([ { state: { isSuccess: true, isPlaceholderData: true, data: 'placeholder', }, count: 0, }, { state: { isSuccess: true, isPlaceholderData: true, data: 'placeholder', }, count: 1, }, { state: { isSuccess: true, isPlaceholderData: false, data: 'data', }, count: 1, }, ]) }) it('placeholder data should run through select', async () => { const key1 = queryKey() const states: UseQueryResult[] = [] function Page() { const state = useQuery(key1, () => 1, { placeholderData: 23, select: (data) => String(data * 2), }) states.push(state) return (

Data: {state.data}

Status: {state.status}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Data: 2')) expect(states).toMatchObject([ { isSuccess: true, isPlaceholderData: true, data: '46', }, { isSuccess: true, isPlaceholderData: false, data: '2', }, ]) }) it('placeholder data function result should run through select', async () => { const key1 = queryKey() const states: UseQueryResult[] = [] let placeholderFunctionRunCount = 0 function Page() { const state = useQuery(key1, () => 1, { placeholderData: () => { placeholderFunctionRunCount++ return 23 }, select: (data) => String(data * 2), }) states.push(state) return (

Data: {state.data}

Status: {state.status}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Data: 2')) rendered.rerender() expect(states).toMatchObject([ { isSuccess: true, isPlaceholderData: true, data: '46', }, { isSuccess: true, isPlaceholderData: false, data: '2', }, { isSuccess: true, isPlaceholderData: false, data: '2', }, ]) expect(placeholderFunctionRunCount).toEqual(1) }) it('select should only run when dependencies change if memoized', async () => { const key1 = queryKey() let selectRun = 0 function Page() { const [count, inc] = React.useReducer((prev) => prev + 1, 2) const state = useQuery( key1, async () => { await sleep(10) return 0 }, { select: React.useCallback( (data: number) => { selectRun++ return `selected ${data + count}` }, [count], ), placeholderData: 99, }, ) return (

Data: {state.data}

) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Data: selected 101')) // 99 + 2 expect(selectRun).toBe(1) await waitFor(() => rendered.getByText('Data: selected 2')) // 0 + 2 expect(selectRun).toBe(2) fireEvent.click(rendered.getByRole('button', { name: /inc/i })) await waitFor(() => rendered.getByText('Data: selected 3')) // 0 + 3 expect(selectRun).toBe(3) }) it('select should always return the correct state', async () => { const key1 = queryKey() function Page() { const [count, inc] = React.useReducer((prev) => prev + 1, 2) const [forceValue, forceUpdate] = React.useReducer((prev) => prev + 1, 1) const state = useQuery( key1, async () => { await sleep(10) return 0 }, { select: React.useCallback( (data: number) => { return `selected ${data + count}` }, [count], ), placeholderData: 99, }, ) return (

Data: {state.data}

forceValue: {forceValue}

) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Data: selected 101')) // 99 + 2 await waitFor(() => rendered.getByText('Data: selected 2')) // 0 + 2 fireEvent.click(rendered.getByRole('button', { name: /inc/i })) await waitFor(() => rendered.getByText('Data: selected 3')) // 0 + 3 fireEvent.click(rendered.getByRole('button', { name: /forceUpdate/i })) await waitFor(() => rendered.getByText('forceValue: 2')) // data should still be 3 after an independent re-render await waitFor(() => rendered.getByText('Data: selected 3')) }) it('select should structurally share data', async () => { const key1 = queryKey() const states: Array> = [] function Page() { const [forceValue, forceUpdate] = React.useReducer((prev) => prev + 1, 1) const state = useQuery( key1, async () => { await sleep(10) return [1, 2] }, { select: (res) => res.map((x) => x + 1), }, ) React.useEffect(() => { if (state.data) { states.push(state.data) } }, [state.data]) return (

Data: {JSON.stringify(state.data)}

forceValue: {forceValue}

) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Data: [2,3]')) expect(states).toHaveLength(1) fireEvent.click(rendered.getByRole('button', { name: /forceUpdate/i })) await waitFor(() => rendered.getByText('forceValue: 2')) await waitFor(() => rendered.getByText('Data: [2,3]')) // effect should not be triggered again due to structural sharing expect(states).toHaveLength(1) }) it('should cancel the query function when there are no more subscriptions', async () => { const key = queryKey() let cancelFn: jest.Mock = jest.fn() const queryFn = ({ signal }: { signal?: AbortSignal }) => { const promise = new Promise((resolve, reject) => { cancelFn = jest.fn(() => reject('Cancelled')) signal?.addEventListener('abort', cancelFn) sleep(20).then(() => resolve('OK')) }) return promise } function Page() { const state = useQuery(key, queryFn) return (

Status: {state.status}

) } const rendered = renderWithClient( queryClient, , ) await waitFor(() => rendered.getByText('off')) expect(cancelFn).toHaveBeenCalled() }) it('should cancel the query if the signal was consumed and there are no more subscriptions', async () => { const key = queryKey() const states: UseQueryResult[] = [] const queryFn: QueryFunction = async ( ctx, ) => { const [, limit] = ctx.queryKey const value = limit % 2 && ctx.signal ? 'abort' : `data ${limit}` await sleep(25) return value } function Page(props: { limit: number }) { const state = useQuery([key, props.limit], queryFn) states[props.limit] = state return (

Status: {state.status}

data: {state.data}

) } const rendered = renderWithClient( queryClient, , ) await waitFor(() => rendered.getByText('off')) await sleep(20) await waitFor(() => expect(states).toHaveLength(4)) expect(queryCache.find([key, 0])?.state).toMatchObject({ data: 'data 0', status: 'success', dataUpdateCount: 1, }) expect(queryCache.find([key, 1])?.state).toMatchObject({ data: undefined, status: 'loading', fetchStatus: 'idle', }) expect(queryCache.find([key, 2])?.state).toMatchObject({ data: 'data 2', status: 'success', dataUpdateCount: 1, }) expect(queryCache.find([key, 3])?.state).toMatchObject({ data: undefined, status: 'loading', fetchStatus: 'idle', }) }) it('should refetch when quickly switching to a failed query', async () => { const key = queryKey() const states: UseQueryResult[] = [] const queryFn = async () => { await sleep(50) return 'OK' } function Page() { const [id, setId] = React.useState(1) const [hasChanged, setHasChanged] = React.useState(false) const state = useQuery([key, id], queryFn) states.push(state) React.useEffect(() => { setId((prevId) => (prevId === 1 ? 2 : 1)) setHasChanged(true) }, [hasChanged]) return null } renderWithClient(queryClient, ) await sleep(100) expect(states.length).toBe(4) // Load query 1 expect(states[0]).toMatchObject({ status: 'loading', error: null, }) // Load query 2 expect(states[1]).toMatchObject({ status: 'loading', error: null, }) // Load query 1 expect(states[2]).toMatchObject({ status: 'loading', error: null, }) // Loaded query 1 expect(states[3]).toMatchObject({ status: 'success', error: null, }) }) it('should update query state and refetch when reset with resetQueries', async () => { const key = queryKey() const states: UseQueryResult[] = [] let count = 0 function Page() { const state = useQuery( key, async () => { await sleep(10) count++ return count }, { staleTime: Infinity }, ) states.push(state) return (
data: {state.data ?? 'null'}
isFetching: {state.isFetching}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: 1')) fireEvent.click(rendered.getByRole('button', { name: /reset/i })) await waitFor(() => expect(states.length).toBe(4)) await waitFor(() => rendered.getByText('data: 2')) expect(count).toBe(2) expect(states[0]).toMatchObject({ data: undefined, isLoading: true, isFetching: true, isSuccess: false, isStale: true, }) expect(states[1]).toMatchObject({ data: 1, isLoading: false, isFetching: false, isSuccess: true, isStale: false, }) expect(states[2]).toMatchObject({ data: undefined, isLoading: true, isFetching: true, isSuccess: false, isStale: true, }) expect(states[3]).toMatchObject({ data: 2, isLoading: false, isFetching: false, isSuccess: true, isStale: false, }) }) it('should update query state and not refetch when resetting a disabled query with resetQueries', async () => { const key = queryKey() const states: UseQueryResult[] = [] let count = 0 function Page() { const state = useQuery( key, async () => { await sleep(10) count++ return count }, { staleTime: Infinity, enabled: false, notifyOnChangeProps: 'all' }, ) states.push(state) const { refetch } = state return (
data: {state.data ?? 'null'}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: null')) fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) await waitFor(() => rendered.getByText('data: 1')) fireEvent.click(rendered.getByRole('button', { name: /reset/i })) await waitFor(() => rendered.getByText('data: null')) await waitFor(() => expect(states.length).toBe(4)) expect(count).toBe(1) expect(states[0]).toMatchObject({ data: undefined, isLoading: true, isFetching: false, isSuccess: false, isStale: true, }) expect(states[1]).toMatchObject({ data: undefined, isLoading: true, isFetching: true, isSuccess: false, isStale: true, }) expect(states[2]).toMatchObject({ data: 1, isLoading: false, isFetching: false, isSuccess: true, isStale: false, }) expect(states[3]).toMatchObject({ data: undefined, isLoading: true, isFetching: false, isSuccess: false, isStale: true, }) }) it('should only call the query hash function once each render', async () => { const key = queryKey() let hashes = 0 let renders = 0 function queryKeyHashFn(x: any) { hashes++ return JSON.stringify(x) } function Page() { React.useEffect(() => { renders++ }) useQuery(key, () => 'test', { queryKeyHashFn }) return null } renderWithClient(queryClient, ) await sleep(10) expect(renders).toBe(hashes) }) it('should refetch when changed enabled to true in error state', async () => { const queryFn = jest.fn() queryFn.mockImplementation(async () => { await sleep(10) return Promise.reject(new Error('Suspense Error Bingo')) }) function Page({ enabled }: { enabled: boolean }) { const { error, isLoading } = useQuery(['key'], queryFn, { enabled, retry: false, retryOnMount: false, refetchOnMount: false, refetchOnWindowFocus: false, }) if (isLoading) { return
status: loading
} if (error instanceof Error) { return
error
} return
rendered
} function App() { const [enabled, toggle] = React.useReducer((x) => !x, true) return (
) } const rendered = renderWithClient(queryClient, ) // initial state check rendered.getByText('status: loading') // // render error state component await waitFor(() => rendered.getByText('error')) expect(queryFn).toBeCalledTimes(1) // change to enabled to false fireEvent.click(rendered.getByLabelText('retry')) await waitFor(() => rendered.getByText('error')) expect(queryFn).toBeCalledTimes(1) // // change to enabled to true fireEvent.click(rendered.getByLabelText('retry')) expect(queryFn).toBeCalledTimes(2) }) it('should refetch when query key changed when previous status is error', async () => { function Page({ id }: { id: number }) { const { error, isLoading } = useQuery( [id], async () => { await sleep(10) if (id % 2 === 1) { return Promise.reject(new Error('Error')) } else { return 'data' } }, { retry: false, retryOnMount: false, refetchOnMount: false, refetchOnWindowFocus: false, }, ) if (isLoading) { return
status: loading
} if (error instanceof Error) { return
error
} return
rendered
} function App() { const [id, changeId] = React.useReducer((x) => x + 1, 1) return (
) } const rendered = renderWithClient(queryClient, ) // initial state check rendered.getByText('status: loading') // render error state component await waitFor(() => rendered.getByText('error')) // change to unmount query fireEvent.click(rendered.getByLabelText('change')) await waitFor(() => rendered.getByText('rendered')) // change to mount new query fireEvent.click(rendered.getByLabelText('change')) await waitFor(() => rendered.getByText('error')) }) it('should refetch when query key changed when switching between erroneous queries', async () => { function Page({ id }: { id: boolean }) { const { error, isFetching } = useQuery( [id], async () => { await sleep(10) return Promise.reject(new Error('Error')) }, { retry: false, retryOnMount: false, refetchOnMount: false, refetchOnWindowFocus: false, }, ) if (isFetching) { return
status: fetching
} if (error instanceof Error) { return
error
} return
rendered
} function App() { const [value, toggle] = React.useReducer((x) => !x, true) return (
) } const rendered = renderWithClient(queryClient, ) // initial state check rendered.getByText('status: fetching') // render error state component await waitFor(() => rendered.getByText('error')) // change to mount second query fireEvent.click(rendered.getByLabelText('change')) await waitFor(() => rendered.getByText('status: fetching')) await waitFor(() => rendered.getByText('error')) // change to mount first query again fireEvent.click(rendered.getByLabelText('change')) await waitFor(() => rendered.getByText('status: fetching')) await waitFor(() => rendered.getByText('error')) }) it('should have no error in loading state when refetching after error occurred', async () => { const key = queryKey() const states: UseQueryResult[] = [] const error = new Error('oops') let count = 0 function Page() { const state = useQuery( key, async () => { await sleep(10) if (count === 0) { count++ throw error } return 5 }, { retry: false, }, ) states.push(state) if (state.isLoading) { return
status: loading
} if (state.error instanceof Error) { return (
error
) } return
data: {state.data}
} const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('error')) fireEvent.click(rendered.getByRole('button', { name: 'refetch' })) await waitFor(() => rendered.getByText('data: 5')) await waitFor(() => expect(states.length).toBe(4)) expect(states[0]).toMatchObject({ status: 'loading', data: undefined, error: null, }) expect(states[1]).toMatchObject({ status: 'error', data: undefined, error, }) expect(states[2]).toMatchObject({ status: 'loading', data: undefined, error: null, }) expect(states[3]).toMatchObject({ status: 'success', data: 5, error: null, }) }) describe('networkMode online', () => { it('online queries should not start fetching if you are offline', async () => { const onlineMock = mockNavigatorOnLine(false) const key = queryKey() const states: Array = [] function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) return 'data' }, }) React.useEffect(() => { states.push(state.fetchStatus) }) return (
status: {state.status}, isPaused: {String(state.isPaused)}
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('status: loading, isPaused: true')) onlineMock.mockReturnValue(true) window.dispatchEvent(new Event('online')) await waitFor(() => rendered.getByText('status: success, isPaused: false'), ) await waitFor(() => { expect(rendered.getByText('data: data')).toBeInTheDocument() }) expect(states).toEqual(['paused', 'fetching', 'idle']) onlineMock.mockRestore() }) it('online queries should not refetch if you are offline', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}, failureCount: {state.failureCount}
failureReason: {state.failureReason ?? 'null'}
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: data1')) const onlineMock = mockNavigatorOnLine(false) fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await waitFor(() => rendered.getByText( 'status: success, fetchStatus: paused, failureCount: 0', ), ) await waitFor(() => rendered.getByText('failureReason: null')) onlineMock.mockReturnValue(true) window.dispatchEvent(new Event('online')) await waitFor(() => rendered.getByText( 'status: success, fetchStatus: fetching, failureCount: 0', ), ) await waitFor(() => rendered.getByText('failureReason: null')) await waitFor(() => rendered.getByText( 'status: success, fetchStatus: idle, failureCount: 0', ), ) await waitFor(() => rendered.getByText('failureReason: null')) await waitFor(() => { expect(rendered.getByText('data: data2')).toBeInTheDocument() }) onlineMock.mockRestore() }) it('online queries should not refetch if you are offline and refocus', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: data1')) const onlineMock = mockNavigatorOnLine(false) fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await waitFor(() => rendered.getByText('status: success, fetchStatus: paused'), ) window.dispatchEvent(new FocusEvent('focus')) await sleep(15) await waitFor(() => expect(rendered.queryByText('data: data2')).not.toBeInTheDocument(), ) expect(count).toBe(1) onlineMock.mockRestore() }) it('online queries should not refetch while already paused', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}
data: {state.data}
) } const onlineMock = mockNavigatorOnLine(false) const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('status: loading, fetchStatus: paused'), ) fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await sleep(15) // invalidation should not trigger a refetch await waitFor(() => rendered.getByText('status: loading, fetchStatus: paused'), ) expect(count).toBe(0) onlineMock.mockRestore() }) it('online queries should not refetch while already paused if data is in the cache', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, initialData: 'initial', }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}
data: {state.data}
) } const onlineMock = mockNavigatorOnLine(false) const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('status: success, fetchStatus: paused'), ) await waitFor(() => { expect(rendered.getByText('data: initial')).toBeInTheDocument() }) fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await sleep(15) // invalidation should not trigger a refetch await waitFor(() => rendered.getByText('status: success, fetchStatus: paused'), ) expect(count).toBe(0) onlineMock.mockRestore() }) it('online queries should not get stuck in fetching state when pausing multiple times', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, initialData: 'initial', }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}
data: {state.data}
) } const onlineMock = mockNavigatorOnLine(false) const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('status: success, fetchStatus: paused'), ) await waitFor(() => { expect(rendered.getByText('data: initial')).toBeInTheDocument() }) // triggers one pause fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await sleep(15) await waitFor(() => rendered.getByText('status: success, fetchStatus: paused'), ) // triggers a second pause act(() => { window.dispatchEvent(new FocusEvent('focus')) }) onlineMock.mockReturnValue(true) act(() => { window.dispatchEvent(new Event('online')) }) await waitFor(() => rendered.getByText('status: success, fetchStatus: idle'), ) await waitFor(() => { expect(rendered.getByText('data: data1')).toBeInTheDocument() }) expect(count).toBe(1) onlineMock.mockRestore() }) it('online queries should pause retries if you are offline', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async (): Promise => { count++ await sleep(10) throw new Error('failed' + count) }, retry: 2, retryDelay: 10, }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}, failureCount: {state.failureCount}
failureReason: {state.failureReason?.message ?? 'null'}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText( 'status: loading, fetchStatus: fetching, failureCount: 1', ), ) await waitFor(() => rendered.getByText('failureReason: failed1')) const onlineMock = mockNavigatorOnLine(false) await sleep(20) await waitFor(() => rendered.getByText( 'status: loading, fetchStatus: paused, failureCount: 1', ), ) await waitFor(() => rendered.getByText('failureReason: failed1')) expect(count).toBe(1) onlineMock.mockReturnValue(true) window.dispatchEvent(new Event('online')) await waitFor(() => rendered.getByText('status: error, fetchStatus: idle, failureCount: 3'), ) await waitFor(() => rendered.getByText('failureReason: failed3')) expect(count).toBe(3) onlineMock.mockRestore() }) it('online queries should fetch if paused and we go online even if already unmounted (because not cancelled)', async () => { const key = queryKey() let count = 0 function Component() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}
data: {state.data}
) } function Page() { const [show, setShow] = React.useState(true) return (
{show && }
) } const onlineMock = mockNavigatorOnLine(false) const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('status: loading, fetchStatus: paused'), ) fireEvent.click(rendered.getByRole('button', { name: /hide/i })) onlineMock.mockReturnValue(true) window.dispatchEvent(new Event('online')) await waitFor(() => { expect(queryClient.getQueryState(key)).toMatchObject({ fetchStatus: 'idle', status: 'success', }) }) // give it a bit more time to make sure queryFn is not called again await sleep(15) expect(count).toBe(1) onlineMock.mockRestore() }) it('online queries should not fetch if paused and we go online when cancelled and no refetchOnReconnect', async () => { const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data' + count }, refetchOnReconnect: false, }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}
data: {state.data}
) } const onlineMock = mockNavigatorOnLine(false) const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('status: loading, fetchStatus: paused'), ) fireEvent.click(rendered.getByRole('button', { name: /cancel/i })) await waitFor(() => rendered.getByText('status: loading, fetchStatus: idle'), ) expect(count).toBe(0) onlineMock.mockReturnValue(true) window.dispatchEvent(new Event('online')) await sleep(15) await waitFor(() => rendered.getByText('status: loading, fetchStatus: idle'), ) expect(count).toBe(0) onlineMock.mockRestore() }) it('online queries should not fetch if paused and we go online if already unmounted when signal consumed', async () => { const key = queryKey() let count = 0 function Component() { const state = useQuery({ queryKey: key, queryFn: async ({ signal }) => { count++ await sleep(10) return `${signal ? 'signal' : 'data'}${count}` }, }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}
data: {state.data}
) } function Page() { const [show, setShow] = React.useState(true) return (
{show && }
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('status: success, fetchStatus: idle'), ) const onlineMock = mockNavigatorOnLine(false) fireEvent.click(rendered.getByRole('button', { name: /invalidate/i })) await waitFor(() => rendered.getByText('status: success, fetchStatus: paused'), ) fireEvent.click(rendered.getByRole('button', { name: /hide/i })) await sleep(15) onlineMock.mockReturnValue(true) window.dispatchEvent(new Event('online')) await sleep(15) expect(queryClient.getQueryState(key)).toMatchObject({ fetchStatus: 'idle', status: 'success', }) expect(count).toBe(1) onlineMock.mockRestore() }) }) describe('networkMode always', () => { it('always queries should start fetching even if you are offline', async () => { const onlineMock = mockNavigatorOnLine(false) const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { count++ await sleep(10) return 'data ' + count }, networkMode: 'always', }) return (
status: {state.status}, isPaused: {String(state.isPaused)}
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('status: success, isPaused: false'), ) await waitFor(() => { expect(rendered.getByText('data: data 1')).toBeInTheDocument() }) onlineMock.mockRestore() }) it('always queries should not pause retries', async () => { const onlineMock = mockNavigatorOnLine(false) const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async (): Promise => { count++ await sleep(10) throw new Error('error ' + count) }, networkMode: 'always', retry: 1, retryDelay: 5, }) return (
status: {state.status}, isPaused: {String(state.isPaused)}
error: {state.error instanceof Error && state.error.message}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('status: error, isPaused: false')) await waitFor(() => { expect(rendered.getByText('error: error 2')).toBeInTheDocument() }) expect(count).toBe(2) onlineMock.mockRestore() }) }) describe('networkMode offlineFirst', () => { it('offlineFirst queries should start fetching if you are offline, but pause retries', async () => { const onlineMock = mockNavigatorOnLine(false) const key = queryKey() let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async (): Promise => { count++ await sleep(10) throw new Error('failed' + count) }, retry: 2, retryDelay: 1, networkMode: 'offlineFirst', }) return (
status: {state.status}, fetchStatus: {state.fetchStatus}, failureCount: {state.failureCount}
failureReason: {state.failureReason?.message ?? 'null'}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText( 'status: loading, fetchStatus: paused, failureCount: 1', ), ) await waitFor(() => rendered.getByText('failureReason: failed1')) expect(count).toBe(1) onlineMock.mockReturnValue(true) window.dispatchEvent(new Event('online')) await waitFor(() => rendered.getByText('status: error, fetchStatus: idle, failureCount: 3'), ) await waitFor(() => rendered.getByText('failureReason: failed3')) expect(count).toBe(3) onlineMock.mockRestore() }) }) it('it should have status=error on mount when a query has failed', async () => { const key = queryKey() const states: UseQueryResult[] = [] const error = new Error('oops') const queryFn = async (): Promise => { throw error } function Page() { const state = useQuery(key, queryFn, { retry: false, retryOnMount: false, }) states.push(state) return <> } await queryClient.prefetchQuery(key, queryFn) renderWithClient(queryClient, ) await waitFor(() => expect(states).toHaveLength(1)) expect(states[0]).toMatchObject({ status: 'error', error, }) }) it('setQueryData - should not call onSuccess callback of active observers', async () => { const key = queryKey() const onSuccess = jest.fn() function Page() { const state = useQuery(key, () => 'data', { onSuccess }) return (
data: {state.data}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: data')) fireEvent.click(rendered.getByRole('button', { name: /setQueryData/i })) await waitFor(() => rendered.getByText('data: newData')) expect(onSuccess).toHaveBeenCalledTimes(1) expect(onSuccess).toHaveBeenCalledWith('data') }) it('setQueryData - should respect updatedAt', async () => { const key = queryKey() function Page() { const state = useQuery(key, () => 'data') return (
data: {state.data}
dataUpdatedAt: {state.dataUpdatedAt}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('data: data')) fireEvent.click(rendered.getByRole('button', { name: /setQueryData/i })) await waitFor(() => rendered.getByText('data: newData')) await waitFor(() => { expect(rendered.getByText('dataUpdatedAt: 100')).toBeInTheDocument() }) }) it('errorUpdateCount should increased on each fetch failure', async () => { const key = queryKey() const error = new Error('oops') function Page() { const { refetch, errorUpdateCount } = useQuery( key, async (): Promise => { throw error }, { retry: false, }, ) return (
data: {errorUpdateCount}
) } const rendered = renderWithClient(queryClient, ) const fetchBtn = rendered.getByRole('button', { name: 'refetch' }) await waitFor(() => rendered.getByText('data: 1')) fireEvent.click(fetchBtn) await waitFor(() => rendered.getByText('data: 2')) fireEvent.click(fetchBtn) await waitFor(() => rendered.getByText('data: 3')) }) it('should be notified of updates between create and subscribe', async () => { const key = queryKey() function Page() { const mounted = React.useRef(false) const { data, status } = useQuery({ enabled: false, queryKey: key, queryFn: async () => { await sleep(10) return 5 }, }) // this simulates a synchronous update between the time the query is created // and the time it is subscribed to that could be missed otherwise if (!mounted.current) { mounted.current = true queryClient.setQueryData(key, 1) } return (
status: {status} data: {data}
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('status: success')) await waitFor(() => rendered.getByText('data: 1')) }) })