diff --git a/docs/framework/react/reference/useInfiniteQuery.md b/docs/framework/react/reference/useInfiniteQuery.md index eab9149f698..e47990ec3c6 100644 --- a/docs/framework/react/reference/useInfiniteQuery.md +++ b/docs/framework/react/reference/useInfiniteQuery.md @@ -54,7 +54,7 @@ The options for `useInfiniteQuery` are identical to the [`useQuery` hook](../use **Returns** -The returned properties for `useInfiniteQuery` are identical to the [`useQuery` hook](../useQuery), with the addition of the following and a small difference in `isRefetching`: +The returned properties for `useInfiniteQuery` are identical to the [`useQuery` hook](../useQuery), with the addition of the following properties and a small difference in `isRefetching` and `isRefetchError`: - `data.pages: TData[]` - Array containing all pages. @@ -66,19 +66,24 @@ The returned properties for `useInfiniteQuery` are identical to the [`useQuery` - Will be `true` while fetching the previous page with `fetchPreviousPage`. - `fetchNextPage: (options?: FetchNextPageOptions) => Promise` - This function allows you to fetch the next "page" of results. - `getNextPageParam`. - - `options.cancelRefetch: boolean` if set to `true`, calling `fetchNextPage` repeatedly will invoke `fetchPage` every time, whether the previous + - `options.cancelRefetch: boolean` if set to `true`, calling `fetchNextPage` repeatedly will invoke `queryFn` every time, whether the previous invocation has resolved or not. Also, the result from previous invocations will be ignored. If set to `false`, calling `fetchNextPage` repeatedly won't have any effect until the first invocation has resolved. Default is `true`. - `fetchPreviousPage: (options?: FetchPreviousPageOptions) => Promise` - This function allows you to fetch the previous "page" of results. - `options.cancelRefetch: boolean` same as for `fetchNextPage`. - `hasNextPage: boolean` - - This will be `true` if there is a next page to be fetched (known via the `getNextPageParam` option). + - Will be `true` if there is a next page to be fetched (known via the `getNextPageParam` option). - `hasPreviousPage: boolean` - - This will be `true` if there is a previous page to be fetched (known via the `getPreviousPageParam` option). + - Will be `true` if there is a previous page to be fetched (known via the `getPreviousPageParam` option). +- `isFetchNextPageError: boolean` + - Will be `true` if the query failed while fetching the next page. +- `isFetchPreviousPageError: boolean` + - Will be `true` if the query failed while fetching the previous page. - `isRefetching: boolean` - - Is `true` whenever a background refetch is in-flight, which _does not_ include initial `pending` or fetching of next or previous page + - Will be `true` whenever a background refetch is in-flight, which _does not_ include initial `pending` or fetching of next or previous page - Is the same as `isFetching && !isPending && !isFetchingNextPage && !isFetchingPreviousPage` +- `isRefetchError: boolean` + - Will be `true` if the query failed while refetching a page. Keep in mind that imperative fetch calls, such as `fetchNextPage`, may interfere with the default refetch behaviour, resulting in outdated data. Make sure to call these functions only in response to user actions, or add conditions like `hasNextPage && !isFetching`. diff --git a/packages/query-core/src/__tests__/infiniteQueryObserver.test-d.tsx b/packages/query-core/src/__tests__/infiniteQueryObserver.test-d.tsx index eafad32f7f7..611bdf17198 100644 --- a/packages/query-core/src/__tests__/infiniteQueryObserver.test-d.tsx +++ b/packages/query-core/src/__tests__/infiniteQueryObserver.test-d.tsx @@ -51,6 +51,8 @@ describe('InfiniteQueryObserver', () => { expectTypeOf(result.data).toEqualTypeOf>() expectTypeOf(result.error).toEqualTypeOf() expectTypeOf(result.status).toEqualTypeOf<'error'>() + expectTypeOf(result.isFetchNextPageError).toEqualTypeOf() + expectTypeOf(result.isFetchPreviousPageError).toEqualTypeOf() } if (result.isSuccess) { diff --git a/packages/query-core/src/infiniteQueryObserver.ts b/packages/query-core/src/infiniteQueryObserver.ts index 4f8ac1e38c2..59d6efa259d 100644 --- a/packages/query-core/src/infiniteQueryObserver.ts +++ b/packages/query-core/src/infiniteQueryObserver.ts @@ -10,6 +10,7 @@ import type { FetchNextPageOptions, FetchPreviousPageOptions, InfiniteData, + InfiniteQueryObserverBaseResult, InfiniteQueryObserverOptions, InfiniteQueryObserverResult, QueryKey, @@ -145,26 +146,33 @@ export class InfiniteQueryObserver< >, ): InfiniteQueryObserverResult { const { state } = query - const result = super.createResult(query, options) + const parentResult = super.createResult(query, options) - const { isFetching, isRefetching } = result + const { isFetching, isRefetching, isError, isRefetchError } = parentResult + const fetchDirection = state.fetchMeta?.fetchMore?.direction - const isFetchingNextPage = - isFetching && state.fetchMeta?.fetchMore?.direction === 'forward' + const isFetchNextPageError = isError && fetchDirection === 'forward' + const isFetchingNextPage = isFetching && fetchDirection === 'forward' - const isFetchingPreviousPage = - isFetching && state.fetchMeta?.fetchMore?.direction === 'backward' + const isFetchPreviousPageError = isError && fetchDirection === 'backward' + const isFetchingPreviousPage = isFetching && fetchDirection === 'backward' - return { - ...result, + const result: InfiniteQueryObserverBaseResult = { + ...parentResult, fetchNextPage: this.fetchNextPage, fetchPreviousPage: this.fetchPreviousPage, hasNextPage: hasNextPage(options, state.data), hasPreviousPage: hasPreviousPage(options, state.data), + isFetchNextPageError, isFetchingNextPage, + isFetchPreviousPageError, isFetchingPreviousPage, + isRefetchError: + isRefetchError && !isFetchNextPageError && !isFetchPreviousPageError, isRefetching: isRefetching && !isFetchingNextPage && !isFetchingPreviousPage, } + + return result as InfiniteQueryObserverResult } } diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 82c4efc629d..33174eedd89 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -473,6 +473,13 @@ export interface ResultOptions { } export interface RefetchOptions extends ResultOptions { + /** + * If set to `true`, a currently running request will be cancelled before a new request is made + * + * If set to `false`, no refetch will be made if there is already a request running. + * + * Defaults to `true`. + */ cancelRefetch?: boolean } @@ -486,10 +493,26 @@ export interface InvalidateOptions extends RefetchOptions {} export interface ResetOptions extends RefetchOptions {} export interface FetchNextPageOptions extends ResultOptions { + /** + * If set to `true`, calling `fetchNextPage` repeatedly will invoke `queryFn` every time, + * whether the previous invocation has resolved or not. Also, the result from previous invocations will be ignored. + * + * If set to `false`, calling `fetchNextPage` repeatedly won't have any effect until the first invocation has resolved. + * + * Defaults to `true`. + */ cancelRefetch?: boolean } export interface FetchPreviousPageOptions extends ResultOptions { + /** + * If set to `true`, calling `fetchPreviousPage` repeatedly will invoke `queryFn` every time, + * whether the previous invocation has resolved or not. Also, the result from previous invocations will be ignored. + * + * If set to `false`, calling `fetchPreviousPage` repeatedly won't have any effect until the first invocation has resolved. + * + * Defaults to `true`. + */ cancelRefetch?: boolean } @@ -712,15 +735,41 @@ export interface InfiniteQueryObserverBaseResult< TData = unknown, TError = DefaultError, > extends QueryObserverBaseResult { + /** + * This function allows you to fetch the next "page" of results. + */ fetchNextPage: ( options?: FetchNextPageOptions, ) => Promise> + /** + * This function allows you to fetch the previous "page" of results. + */ fetchPreviousPage: ( options?: FetchPreviousPageOptions, ) => Promise> + /** + * Will be `true` if there is a next page to be fetched (known via the `getNextPageParam` option). + */ hasNextPage: boolean + /** + * Will be `true` if there is a previous page to be fetched (known via the `getPreviousPageParam` option). + */ hasPreviousPage: boolean + /** + * Will be `true` if the query failed while fetching the next page. + */ + isFetchNextPageError: boolean + /** + * Will be `true` while fetching the next page with `fetchNextPage`. + */ isFetchingNextPage: boolean + /** + * Will be `true` if the query failed while fetching the previous page. + */ + isFetchPreviousPageError: boolean + /** + * Will be `true` while fetching the previous page with `fetchPreviousPage`. + */ isFetchingPreviousPage: boolean } @@ -734,6 +783,8 @@ export interface InfiniteQueryObserverPendingResult< isPending: true isLoadingError: false isRefetchError: false + isFetchNextPageError: false + isFetchPreviousPageError: false isSuccess: false status: 'pending' } @@ -749,6 +800,8 @@ export interface InfiniteQueryObserverLoadingResult< isLoading: true isLoadingError: false isRefetchError: false + isFetchNextPageError: false + isFetchPreviousPageError: false isSuccess: false status: 'pending' } @@ -764,6 +817,8 @@ export interface InfiniteQueryObserverLoadingErrorResult< isLoading: false isLoadingError: true isRefetchError: false + isFetchNextPageError: false + isFetchPreviousPageError: false isSuccess: false status: 'error' } @@ -779,6 +834,8 @@ export interface InfiniteQueryObserverRefetchErrorResult< isLoading: false isLoadingError: false isRefetchError: true + isFetchNextPageError: false + isFetchPreviousPageError: false isSuccess: false status: 'error' } @@ -794,6 +851,8 @@ export interface InfiniteQueryObserverSuccessResult< isLoading: false isLoadingError: false isRefetchError: false + isFetchNextPageError: false + isFetchPreviousPageError: false isSuccess: true status: 'success' } diff --git a/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx b/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx index b0b73f7a849..b4024f3c62e 100644 --- a/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx +++ b/packages/react-query/src/__tests__/useInfiniteQuery.test.tsx @@ -81,7 +81,9 @@ describe('useInfiniteQuery', () => { isFetchedAfterMount: false, isFetching: true, isPaused: false, + isFetchNextPageError: false, isFetchingNextPage: false, + isFetchPreviousPageError: false, isFetchingPreviousPage: false, isLoading: true, isPending: true, @@ -114,7 +116,9 @@ describe('useInfiniteQuery', () => { isFetchedAfterMount: true, isFetching: false, isPaused: false, + isFetchNextPageError: false, isFetchingNextPage: false, + isFetchPreviousPageError: false, isFetchingPreviousPage: false, isLoading: false, isPending: false, @@ -595,6 +599,279 @@ describe('useInfiniteQuery', () => { }) }) + it('should return the correct states when refetch fails', async () => { + const key = queryKey() + const states: Array>> = [] + let isRefetch = false + + function Page() { + const state = useInfiniteQuery({ + queryKey: key, + queryFn: async ({ pageParam }) => { + await sleep(10) + if (isRefetch) { + throw new Error() + } else { + return Number(pageParam) + } + }, + initialPageParam: 10, + getPreviousPageParam: (firstPage) => firstPage - 1, + getNextPageParam: (lastPage) => lastPage + 1, + notifyOnChangeProps: 'all', + retry: false, + }) + + states.push(state) + + return ( +
+ +
data: {state.data?.pages.join(',') ?? 'null'}
+
isFetching: {String(state.isFetching)}
+
+ ) + } + + const rendered = renderWithClient(queryClient, ) + + await waitFor(() => rendered.getByText('data: 10')) + fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) + + await waitFor(() => rendered.getByText('isFetching: false')) + await waitFor(() => expect(states.length).toBe(4)) + + // Initial fetch + expect(states[0]).toMatchObject({ + data: undefined, + isFetching: true, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + // Initial fetch done + expect(states[1]).toMatchObject({ + data: { pages: [10] }, + isFetching: false, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + // Refetch + expect(states[2]).toMatchObject({ + data: { pages: [10] }, + isFetching: true, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: true, + }) + // Refetch failed + expect(states[3]).toMatchObject({ + data: { pages: [10] }, + isFetching: false, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: true, + isRefetching: false, + }) + }) + + it('should return the correct states when fetchNextPage fails', async () => { + const key = queryKey() + const states: Array>> = [] + + function Page() { + const state = useInfiniteQuery({ + queryKey: key, + queryFn: async ({ pageParam }) => { + await sleep(10) + if (pageParam !== 10) { + throw new Error() + } else { + return Number(pageParam) + } + }, + initialPageParam: 10, + getPreviousPageParam: (firstPage) => firstPage - 1, + getNextPageParam: (lastPage) => lastPage + 1, + notifyOnChangeProps: 'all', + retry: false, + }) + + states.push(state) + + return ( +
+ +
data: {state.data?.pages.join(',') ?? 'null'}
+
isFetching: {String(state.isFetching)}
+
+ ) + } + + const rendered = renderWithClient(queryClient, ) + + await waitFor(() => rendered.getByText('data: 10')) + fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) + + await waitFor(() => rendered.getByText('isFetching: false')) + await waitFor(() => expect(states.length).toBe(4)) + + // Initial fetch + expect(states[0]).toMatchObject({ + data: undefined, + isFetching: true, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + // Initial fetch done + expect(states[1]).toMatchObject({ + data: { pages: [10] }, + isFetching: false, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + // Fetch next page + expect(states[2]).toMatchObject({ + data: { pages: [10] }, + isFetching: true, + isFetchNextPageError: false, + isFetchingNextPage: true, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + // Fetch next page failed + expect(states[3]).toMatchObject({ + data: { pages: [10] }, + isFetching: false, + isFetchNextPageError: true, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + }) + + it('should return the correct states when fetchPreviousPage fails', async () => { + const key = queryKey() + const states: Array>> = [] + + function Page() { + const state = useInfiniteQuery({ + queryKey: key, + queryFn: async ({ pageParam }) => { + await sleep(10) + if (pageParam !== 10) { + throw new Error() + } else { + return Number(pageParam) + } + }, + initialPageParam: 10, + getPreviousPageParam: (firstPage) => firstPage - 1, + getNextPageParam: (lastPage) => lastPage + 1, + notifyOnChangeProps: 'all', + retry: false, + }) + + states.push(state) + + return ( +
+ +
data: {state.data?.pages.join(',') ?? 'null'}
+
isFetching: {String(state.isFetching)}
+
+ ) + } + + const rendered = renderWithClient(queryClient, ) + + await waitFor(() => rendered.getByText('data: 10')) + fireEvent.click( + rendered.getByRole('button', { name: /fetchPreviousPage/i }), + ) + + await waitFor(() => rendered.getByText('isFetching: false')) + await waitFor(() => expect(states.length).toBe(4)) + + // Initial fetch + expect(states[0]).toMatchObject({ + data: undefined, + isFetching: true, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + // Initial fetch done + expect(states[1]).toMatchObject({ + data: { pages: [10] }, + isFetching: false, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + // Fetch previous page + expect(states[2]).toMatchObject({ + data: { pages: [10] }, + isFetching: true, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: true, + isRefetchError: false, + isRefetching: false, + }) + // Fetch previous page failed + expect(states[3]).toMatchObject({ + data: { pages: [10] }, + isFetching: false, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: true, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + }) + it('should silently cancel any ongoing fetch when fetching more', async () => { const key = queryKey() diff --git a/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx b/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx index 472ae46c528..1e174fe81b9 100644 --- a/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx +++ b/packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx @@ -103,7 +103,9 @@ describe('useInfiniteQuery', () => { isFetchedAfterMount: false, isFetching: true, isPaused: false, + isFetchNextPageError: false, isFetchingNextPage: false, + isFetchPreviousPageError: false, isFetchingPreviousPage: false, isPending: true, isLoading: true, @@ -136,7 +138,9 @@ describe('useInfiniteQuery', () => { isFetchedAfterMount: true, isFetching: false, isPaused: false, + isFetchNextPageError: false, isFetchingNextPage: false, + isFetchPreviousPageError: false, isFetchingPreviousPage: false, isPending: false, isLoading: false, @@ -679,6 +683,330 @@ describe('useInfiniteQuery', () => { }) }) + it('should return the correct states when refetch fails', async () => { + const key = queryKey() + const states: Array< + Partial>> + > = [] + let isRefetch = false + + function Page() { + const state = createInfiniteQuery(() => ({ + queryKey: key, + queryFn: async ({ pageParam }) => { + await sleep(10) + if (isRefetch) { + throw new Error() + } else { + return Number(pageParam) + } + }, + getPreviousPageParam: (firstPage) => firstPage - 1, + getNextPageParam: (lastPage) => lastPage + 1, + initialPageParam: 10, + notifyOnChangeProps: 'all', + retry: false, + })) + + createRenderEffect(() => { + states.push({ + data: state.data ? JSON.parse(JSON.stringify(state.data)) : undefined, + isFetching: state.isFetching, + isFetchNextPageError: state.isFetchNextPageError, + isFetchingNextPage: state.isFetchingNextPage, + isFetchPreviousPageError: state.isFetchPreviousPageError, + isFetchingPreviousPage: state.isFetchingPreviousPage, + isRefetchError: state.isRefetchError, + isRefetching: state.isRefetching, + }) + }) + + return ( +
+ +
data: {state.data?.pages.join(',') ?? 'null'}
+
isFetching: {String(state.isFetching)}
+
+ ) + } + + const rendered = render(() => ( + + + + )) + + await waitFor(() => rendered.getByText('data: 10')) + fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) + + await waitFor(() => rendered.getByText('isFetching: false')) + await waitFor(() => expect(states.length).toBe(4)) + + // Initial fetch + expect(states[0]).toMatchObject({ + data: undefined, + isFetching: true, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + // Initial fetch done + expect(states[1]).toMatchObject({ + data: { pages: [10] }, + isFetching: false, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + // Refetch + expect(states[2]).toMatchObject({ + data: { pages: [10] }, + isFetching: true, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: true, + }) + // Refetch failed + expect(states[3]).toMatchObject({ + data: { pages: [10] }, + isFetching: false, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: true, + isRefetching: false, + }) + }) + + it('should return the correct states when fetchNextPage fails', async () => { + const key = queryKey() + const states: Array< + Partial>> + > = [] + + function Page() { + const state = createInfiniteQuery(() => ({ + queryKey: key, + queryFn: async ({ pageParam }) => { + await sleep(10) + if (pageParam !== 10) { + throw new Error() + } else { + return Number(pageParam) + } + }, + getPreviousPageParam: (firstPage) => firstPage - 1, + getNextPageParam: (lastPage) => lastPage + 1, + initialPageParam: 10, + notifyOnChangeProps: 'all', + retry: false, + })) + + createRenderEffect(() => { + states.push({ + data: state.data ? JSON.parse(JSON.stringify(state.data)) : undefined, + isFetching: state.isFetching, + isFetchNextPageError: state.isFetchNextPageError, + isFetchingNextPage: state.isFetchingNextPage, + isFetchPreviousPageError: state.isFetchPreviousPageError, + isFetchingPreviousPage: state.isFetchingPreviousPage, + isRefetchError: state.isRefetchError, + isRefetching: state.isRefetching, + }) + }) + + return ( +
+ +
data: {state.data?.pages.join(',') ?? 'null'}
+
isFetching: {String(state.isFetching)}
+
+ ) + } + + const rendered = render(() => ( + + + + )) + + await waitFor(() => rendered.getByText('data: 10')) + fireEvent.click(rendered.getByRole('button', { name: /fetchNextPage/i })) + + await waitFor(() => rendered.getByText('isFetching: false')) + await waitFor(() => expect(states.length).toBe(4)) + + // Initial fetch + expect(states[0]).toMatchObject({ + data: undefined, + isFetching: true, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + // Initial fetch done + expect(states[1]).toMatchObject({ + data: { pages: [10] }, + isFetching: false, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + // Fetch next page + expect(states[2]).toMatchObject({ + data: { pages: [10] }, + isFetching: true, + isFetchNextPageError: false, + isFetchingNextPage: true, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + // Fetch next page failed + expect(states[3]).toMatchObject({ + data: { pages: [10] }, + isFetching: false, + isFetchNextPageError: true, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + }) + + it('should return the correct states when fetchPreviousPage fails', async () => { + const key = queryKey() + const states: Array< + Partial>> + > = [] + + function Page() { + const state = createInfiniteQuery(() => ({ + queryKey: key, + queryFn: async ({ pageParam }) => { + await sleep(10) + if (pageParam !== 10) { + throw new Error() + } else { + return Number(pageParam) + } + }, + getPreviousPageParam: (firstPage) => firstPage - 1, + getNextPageParam: (lastPage) => lastPage + 1, + initialPageParam: 10, + notifyOnChangeProps: 'all', + retry: false, + })) + + createRenderEffect(() => { + states.push({ + data: state.data ? JSON.parse(JSON.stringify(state.data)) : undefined, + isFetching: state.isFetching, + isFetchNextPageError: state.isFetchNextPageError, + isFetchingNextPage: state.isFetchingNextPage, + isFetchPreviousPageError: state.isFetchPreviousPageError, + isFetchingPreviousPage: state.isFetchingPreviousPage, + isRefetchError: state.isRefetchError, + isRefetching: state.isRefetching, + }) + }) + + return ( +
+ +
data: {state.data?.pages.join(',') ?? 'null'}
+
isFetching: {String(state.isFetching)}
+
+ ) + } + + const rendered = render(() => ( + + + + )) + + await waitFor(() => rendered.getByText('data: 10')) + fireEvent.click( + rendered.getByRole('button', { name: /fetchPreviousPage/i }), + ) + + await waitFor(() => rendered.getByText('isFetching: false')) + await waitFor(() => expect(states.length).toBe(4)) + + // Initial fetch + expect(states[0]).toMatchObject({ + data: undefined, + isFetching: true, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + // Initial fetch done + expect(states[1]).toMatchObject({ + data: { pages: [10] }, + isFetching: false, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + // Fetch previous page + expect(states[2]).toMatchObject({ + data: { pages: [10] }, + isFetching: true, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: true, + isRefetchError: false, + isRefetching: false, + }) + // Fetch previous page failed + expect(states[3]).toMatchObject({ + data: { pages: [10] }, + isFetching: false, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: true, + isFetchingPreviousPage: false, + isRefetchError: false, + isRefetching: false, + }) + }) + it('should silently cancel any ongoing fetch when fetching more', async () => { const key = queryKey() const states: Array<