import { fireEvent, waitFor } from '@testing-library/react' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import type { UseInfiniteQueryResult, UseQueryResult } from '..' import { QueryCache, QueryErrorResetBoundary, useInfiniteQuery, useQueries, useQuery, useQueryErrorResetBoundary, } from '..' import { createQueryClient, queryKey, renderWithClient, sleep } from './utils' describe("useQuery's in Suspense mode", () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) it('should render the correct amount of times in Suspense mode', async () => { const key = queryKey() const states: UseQueryResult[] = [] let count = 0 let renders = 0 function Page() { renders++ const [stateKey, setStateKey] = React.useState(key) const state = useQuery( stateKey, async () => { count++ await sleep(10) return count }, { suspense: true }, ) states.push(state) return (
) } const rendered = renderWithClient( queryClient, , ) await waitFor(() => rendered.getByText('data: 1')) fireEvent.click(rendered.getByLabelText('toggle')) await waitFor(() => rendered.getByText('data: 2')) expect(renders).toBe(4) expect(states.length).toBe(2) expect(states[0]).toMatchObject({ data: 1, status: 'success' }) expect(states[1]).toMatchObject({ data: 2, status: 'success' }) }) it('should return the correct states for a successful infinite query', async () => { const key = queryKey() const states: UseInfiniteQueryResult[] = [] function Page() { const [multiplier, setMultiplier] = React.useState(1) const state = useInfiniteQuery( [`${key}_${multiplier}`], async ({ pageParam = 1 }) => { await sleep(10) return Number(pageParam * multiplier) }, { suspense: true, getNextPageParam: (lastPage) => lastPage + 1, }, ) states.push(state) return (
data: {state.data?.pages.join(',')}
) } const rendered = renderWithClient( queryClient, , ) await waitFor(() => rendered.getByText('data: 1')) expect(states.length).toBe(1) expect(states[0]).toMatchObject({ data: { pages: [1], pageParams: [undefined] }, status: 'success', }) fireEvent.click(rendered.getByText('next')) await waitFor(() => rendered.getByText('data: 2')) expect(states.length).toBe(2) expect(states[1]).toMatchObject({ data: { pages: [2], pageParams: [undefined] }, status: 'success', }) }) it('should not call the queryFn twice when used in Suspense mode', async () => { const key = queryKey() const queryFn = jest.fn() queryFn.mockImplementation(() => { sleep(10) return 'data' }) function Page() { useQuery([key], queryFn, { suspense: true }) return <>rendered } const rendered = renderWithClient( queryClient, , ) await waitFor(() => rendered.getByText('rendered')) expect(queryFn).toHaveBeenCalledTimes(1) }) it('should remove query instance when component unmounted', async () => { const key = queryKey() function Page() { useQuery( key, () => { sleep(50) return 'data' }, { suspense: true }, ) return <>rendered } function App() { const [show, setShow] = React.useState(false) return ( <> {show && } )} > )} , ) await waitFor(() => rendered.getByText('Loading...')) await waitFor(() => rendered.getByText('error boundary')) await waitFor(() => rendered.getByText('retry')) fireEvent.click(rendered.getByText('retry')) await waitFor(() => rendered.getByText('rendered')) }) it('should retry fetch if the reset error boundary has been reset', async () => { const key = queryKey() let succeed = false function Page() { useQuery( key, async () => { await sleep(10) if (!succeed) { throw new Error('Suspense Error Bingo') } else { return 'data' } }, { retry: false, suspense: true, }, ) return
rendered
} const rendered = renderWithClient( queryClient, {({ reset }) => ( (
error boundary
)} >
)}
, ) await waitFor(() => rendered.getByText('Loading...')) await waitFor(() => rendered.getByText('error boundary')) await waitFor(() => rendered.getByText('retry')) fireEvent.click(rendered.getByText('retry')) await waitFor(() => rendered.getByText('error boundary')) await waitFor(() => rendered.getByText('retry')) succeed = true fireEvent.click(rendered.getByText('retry')) await waitFor(() => rendered.getByText('rendered')) }) it('should refetch when re-mounting', async () => { const key = queryKey() let count = 0 function Component() { const result = useQuery( key, async () => { await sleep(100) count++ return count }, { retry: false, suspense: true, staleTime: 0, }, ) return (
data: {result.data} fetching: {result.isFetching ? 'true' : 'false'}
) } function Page() { const [show, setShow] = React.useState(true) return (
{show && }
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Loading...')) await waitFor(() => rendered.getByText('data: 1')) await waitFor(() => rendered.getByText('fetching: false')) await waitFor(() => rendered.getByText('hide')) fireEvent.click(rendered.getByText('hide')) await waitFor(() => rendered.getByText('show')) fireEvent.click(rendered.getByText('show')) await waitFor(() => rendered.getByText('fetching: true')) await waitFor(() => rendered.getByText('data: 2')) await waitFor(() => rendered.getByText('fetching: false')) }) it('should suspend when switching to a new query', async () => { const key1 = queryKey() const key2 = queryKey() function Component(props: { queryKey: Array }) { const result = useQuery( props.queryKey, async () => { await sleep(100) return props.queryKey }, { retry: false, suspense: true, }, ) return
data: {result.data}
} function Page() { const [key, setKey] = React.useState(key1) return (
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Loading...')) await waitFor(() => rendered.getByText(`data: ${key1}`)) fireEvent.click(rendered.getByText('switch')) await waitFor(() => rendered.getByText('Loading...')) await waitFor(() => rendered.getByText(`data: ${key2}`)) expect( // @ts-expect-error queryClient.getQueryCache().find(key2)!.observers[0].listeners.size, ).toBe(1) }) it('should retry fetch if the reset error boundary has been reset with global hook', async () => { const key = queryKey() let succeed = false function Page() { useQuery( key, async () => { await sleep(10) if (!succeed) { throw new Error('Suspense Error Bingo') } else { return 'data' } }, { retry: false, suspense: true, }, ) return
rendered
} function App() { const { reset } = useQueryErrorResetBoundary() return ( (
error boundary
)} >
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Loading...')) await waitFor(() => rendered.getByText('error boundary')) await waitFor(() => rendered.getByText('retry')) fireEvent.click(rendered.getByText('retry')) await waitFor(() => rendered.getByText('error boundary')) await waitFor(() => rendered.getByText('retry')) succeed = true fireEvent.click(rendered.getByText('retry')) await waitFor(() => rendered.getByText('rendered')) }) it('should throw errors to the error boundary by default', async () => { const key = queryKey() function Page() { useQuery( key, async (): Promise => { await sleep(10) throw new Error('Suspense Error a1x') }, { retry: false, suspense: true, }, ) return
rendered
} function App() { return ( (
error boundary
)} >
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Loading...')) await waitFor(() => rendered.getByText('error boundary')) }) it('should not throw errors to the error boundary when useErrorBoundary: false', async () => { const key = queryKey() function Page() { useQuery( key, async (): Promise => { await sleep(10) throw new Error('Suspense Error a2x') }, { retry: false, suspense: true, useErrorBoundary: false, }, ) return
rendered
} function App() { return ( (
error boundary
)} >
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Loading...')) await waitFor(() => rendered.getByText('rendered')) }) it('should not throw errors to the error boundary when a useErrorBoundary function returns true', async () => { const key = queryKey() function Page() { useQuery( key, async (): Promise => { await sleep(10) return Promise.reject('Remote Error') }, { retry: false, suspense: true, useErrorBoundary: (err) => err !== 'Local Error', }, ) return
rendered
} function App() { return ( (
error boundary
)} >
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Loading...')) await waitFor(() => rendered.getByText('error boundary')) }) it('should not throw errors to the error boundary when a useErrorBoundary function returns false', async () => { const key = queryKey() function Page() { useQuery( key, async (): Promise => { await sleep(10) return Promise.reject('Local Error') }, { retry: false, suspense: true, useErrorBoundary: (err) => err !== 'Local Error', }, ) return
rendered
} function App() { return ( (
error boundary
)} >
) } const rendered = renderWithClient(queryClient, ) await waitFor(() => rendered.getByText('Loading...')) await waitFor(() => rendered.getByText('rendered')) }) it('should not call the queryFn when not enabled', async () => { const key = queryKey() const queryFn = jest.fn, unknown[]>() queryFn.mockImplementation(async () => { await sleep(10) return '23' }) function Page() { const [enabled, setEnabled] = React.useState(false) const result = useQuery([key], queryFn, { suspense: true, enabled }) return (

{result.data}

) } const rendered = renderWithClient( queryClient, , ) expect(queryFn).toHaveBeenCalledTimes(0) fireEvent.click(rendered.getByRole('button', { name: /fire/i })) await waitFor(() => { expect(rendered.getByRole('heading').textContent).toBe('23') }) expect(queryFn).toHaveBeenCalledTimes(1) }) it('should error catched in error boundary without infinite loop', async () => { const key = queryKey() let succeed = true function Page() { const [nonce] = React.useState(0) const queryKeys = [`${key}-${succeed}`] const result = useQuery( queryKeys, async () => { await sleep(10) if (!succeed) { throw new Error('Suspense Error Bingo') } else { return nonce } }, { retry: false, suspense: true, }, ) return (
rendered {result.data}
) } function App() { const { reset } = useQueryErrorResetBoundary() return (
error boundary
} >
) } const rendered = renderWithClient(queryClient, ) // render suspense fallback (Loading...) await waitFor(() => rendered.getByText('Loading...')) // resolve promise -> render Page (rendered) await waitFor(() => rendered.getByText('rendered')) // change query key succeed = false // reset query -> and throw error fireEvent.click(rendered.getByLabelText('fail')) // render error boundary fallback (error boundary) await waitFor(() => rendered.getByText('error boundary')) }) it('should error catched in error boundary without infinite loop when query keys changed', async () => { let succeed = true function Page() { const [key, rerender] = React.useReducer((x) => x + 1, 0) const queryKeys = [key, succeed] const result = useQuery( queryKeys, async () => { await sleep(10) if (!succeed) { throw new Error('Suspense Error Bingo') } else { return 'data' } }, { retry: false, suspense: true, }, ) return (
rendered {result.data}
) } function App() { const { reset } = useQueryErrorResetBoundary() return (
error boundary
} >
) } const rendered = renderWithClient(queryClient, ) // render suspense fallback (Loading...) await waitFor(() => rendered.getByText('Loading...')) // resolve promise -> render Page (rendered) await waitFor(() => rendered.getByText('rendered')) // change promise result to error succeed = false // change query key fireEvent.click(rendered.getByLabelText('fail')) // render error boundary fallback (error boundary) await waitFor(() => rendered.getByText('error boundary')) }) it('should error catched in error boundary without infinite loop when enabled changed', async () => { function Page() { const queryKeys = '1' const [enabled, setEnabled] = React.useState(false) const result = useQuery( [queryKeys], async () => { await sleep(10) throw new Error('Suspense Error Bingo') }, { retry: false, suspense: true, enabled, }, ) return (
rendered {result.data}
) } function App() { const { reset } = useQueryErrorResetBoundary() return (
error boundary
} >
) } const rendered = renderWithClient(queryClient, ) // render empty data with 'rendered' when enabled is false await waitFor(() => rendered.getByText('rendered')) // change enabled to true fireEvent.click(rendered.getByLabelText('fail')) // render pending fallback await waitFor(() => rendered.getByText('Loading...')) // render error boundary fallback (error boundary) await waitFor(() => rendered.getByText('error boundary')) }) it('should render the correct amount of times in Suspense mode when cacheTime is set to 0', async () => { const key = queryKey() let state: UseQueryResult | null = null let count = 0 let renders = 0 function Page() { renders++ state = useQuery( key, async () => { count++ await sleep(10) return count }, { suspense: true, cacheTime: 0 }, ) return (
rendered
) } const rendered = renderWithClient( queryClient, , ) await waitFor(() => expect(state).toMatchObject({ data: 1, status: 'success', }), ) expect(renders).toBe(2) expect(rendered.queryByText('rendered')).not.toBeNull() }) }) describe('useQueries with suspense', () => { const queryClient = createQueryClient() it('should suspend all queries in parallel', async () => { const key1 = queryKey() const key2 = queryKey() const results: string[] = [] function Fallback() { results.push('loading') return
loading
} function Page() { const result = useQueries({ queries: [ { queryKey: key1, queryFn: async () => { results.push('1') await sleep(10) return '1' }, suspense: true, }, { queryKey: key2, queryFn: async () => { results.push('2') await sleep(20) return '2' }, suspense: true, }, ], }) return (

data: {result.map((it) => it.data ?? 'null').join(',')}

) } const rendered = renderWithClient( queryClient, }> , ) await waitFor(() => rendered.getByText('loading')) await waitFor(() => rendered.getByText('data: 1,2')) expect(results).toEqual(['1', '2', 'loading']) }) it('should allow to mix suspense with non-suspense', async () => { const key1 = queryKey() const key2 = queryKey() const results: string[] = [] function Fallback() { results.push('loading') return
loading
} function Page() { const result = useQueries({ queries: [ { queryKey: key1, queryFn: async () => { results.push('1') await sleep(50) return '1' }, suspense: true, }, { queryKey: key2, queryFn: async () => { results.push('2') await sleep(200) return '2' }, staleTime: 1000, suspense: false, }, ], }) return (

data: {result.map((it) => it.data ?? 'null').join(',')}

status: {result.map((it) => it.status).join(',')}

) } const rendered = renderWithClient( queryClient, }> , ) await waitFor(() => rendered.getByText('loading')) await waitFor(() => rendered.getByText('status: success,loading')) await waitFor(() => rendered.getByText('data: 1,null')) await waitFor(() => rendered.getByText('data: 1,2')) expect(results).toEqual(['1', '2', 'loading']) }) it("shouldn't unmount before all promises fetched", async () => { const key1 = queryKey() const key2 = queryKey() const results: string[] = [] const refs: number[] = [] function Fallback() { results.push('loading') return
loading
} function Page() { const ref = React.useRef(Math.random()) const result = useQueries({ queries: [ { queryKey: key1, queryFn: async () => { refs.push(ref.current) results.push('1') await sleep(10) return '1' }, suspense: true, }, { queryKey: key2, queryFn: async () => { refs.push(ref.current) results.push('2') await sleep(20) return '2' }, suspense: true, }, ], }) return (

data: {result.map((it) => it.data ?? 'null').join(',')}

) } const rendered = renderWithClient( queryClient, }> , ) await waitFor(() => rendered.getByText('loading')) expect(refs.length).toBe(2) await waitFor(() => rendered.getByText('data: 1,2')) expect(refs[0]).toBe(refs[1]) }) it('should suspend all queries in parallel - global configuration', async () => { const queryClientSuspenseMode = createQueryClient({ defaultOptions: { queries: { suspense: true, }, }, }) const key1 = queryKey() const key2 = queryKey() const results: string[] = [] function Fallback() { results.push('loading') return
loading
} function Page() { const result = useQueries({ queries: [ { queryKey: key1, queryFn: async () => { results.push('1') await sleep(10) return '1' }, }, { queryKey: key2, queryFn: async () => { results.push('2') await sleep(20) return '2' }, }, ], }) return (

data: {result.map((it) => it.data ?? 'null').join(',')}

) } const rendered = renderWithClient( queryClientSuspenseMode, }> , ) await waitFor(() => rendered.getByText('loading')) await waitFor(() => rendered.getByText('data: 1,2')) expect(results).toEqual(['1', '2', 'loading']) }) })