-
Notifications
You must be signed in to change notification settings - Fork 107
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b1b553c
commit c209c78
Showing
5 changed files
with
324 additions
and
32 deletions.
There are no files selected for viewing
194 changes: 194 additions & 0 deletions
194
src/hooks/use-merged-infinite-queries/__tests__/use-merged-infinite-queries.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
import { act } from 'react-dom/test-utils'; | ||
|
||
import { renderHook, waitFor } from '@/test-utils/rtl'; | ||
|
||
import useMergedInfiniteQueries from '../use-merged-infinite-queries'; | ||
import { type SingleInfiniteQueryOptions } from '../use-merged-infinite-queries.types'; | ||
|
||
type MockAPIResponse = { | ||
entries: Array<number>; | ||
nextPage: number; | ||
}; | ||
|
||
const MOCK_QUERY_CONFIG: Array< | ||
SingleInfiniteQueryOptions<MockAPIResponse, number> | ||
> = [ | ||
{ | ||
queryKey: ['even-numbers'], | ||
queryFn: async ({ pageParam }) => ({ | ||
entries: Array.from({ length: 5 }, (_, i) => pageParam + i * 2), | ||
nextPage: pageParam + 10, | ||
}), | ||
getNextPageParam: (lastPage) => lastPage.nextPage, | ||
initialPageParam: 0, | ||
}, | ||
{ | ||
queryKey: ['odd-numbers'], | ||
queryFn: async ({ pageParam }) => ({ | ||
entries: Array.from({ length: 5 }, (_, i) => pageParam + i * 2), | ||
nextPage: pageParam + 10, | ||
}), | ||
getNextPageParam: (lastPage) => lastPage.nextPage, | ||
initialPageParam: 1, | ||
}, | ||
]; | ||
|
||
const MOCK_QUERY_CONFIG_WITH_ERROR: Array< | ||
SingleInfiniteQueryOptions<MockAPIResponse, number> | ||
> = [ | ||
{ | ||
queryKey: ['even-numbers'], | ||
queryFn: async ({ pageParam }) => ({ | ||
entries: Array.from({ length: 5 }, (_, i) => pageParam + i * 2), | ||
nextPage: pageParam + 10, | ||
}), | ||
getNextPageParam: (lastPage) => lastPage.nextPage, | ||
initialPageParam: 0, | ||
}, | ||
{ | ||
queryKey: ['odd-numbers'], | ||
queryFn: async () => { | ||
throw new Error(`That's odd, something went wrong`); | ||
}, | ||
getNextPageParam: (lastPage) => lastPage.nextPage, | ||
initialPageParam: 1, | ||
}, | ||
]; | ||
|
||
describe(useMergedInfiniteQueries.name, () => { | ||
it('should merge infinite query results, and return correct loading states', async () => { | ||
const { result } = renderHook(() => | ||
useMergedInfiniteQueries({ | ||
queries: MOCK_QUERY_CONFIG, | ||
pageSize: 5, | ||
flattenResponse: (res) => res.entries, | ||
compare: (a, b) => (a < b ? -1 : 1), | ||
}) | ||
); | ||
|
||
expect(result.current[0].isFetching).toStrictEqual(true); | ||
expect(result.current[0].isLoading).toStrictEqual(true); | ||
expect(result.current[0].isFetchingNextPage).toStrictEqual(false); | ||
|
||
await waitFor(() => { | ||
const [mergedResult] = result.current; | ||
expect(mergedResult.data).toStrictEqual([0, 1, 2, 3, 4]); | ||
}); | ||
|
||
const [{ fetchNextPage }] = result.current; | ||
|
||
act(() => { | ||
fetchNextPage(); | ||
}); | ||
|
||
expect(result.current[0].isFetching).toStrictEqual(true); | ||
expect(result.current[0].isLoading).toStrictEqual(false); | ||
expect(result.current[0].isFetchingNextPage).toStrictEqual(true); | ||
|
||
await waitFor(() => { | ||
const [mergedResult] = result.current; | ||
expect(mergedResult.data).toStrictEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); | ||
}); | ||
}); | ||
|
||
it('should return partial query results if one of the queries fails', async () => { | ||
const { result } = renderHook(() => | ||
useMergedInfiniteQueries({ | ||
queries: MOCK_QUERY_CONFIG_WITH_ERROR, | ||
pageSize: 5, | ||
flattenResponse: (res) => res.entries, | ||
compare: (a, b) => (a < b ? -1 : 1), | ||
}) | ||
); | ||
|
||
await waitFor(() => { | ||
const [mergedResult] = result.current; | ||
expect(mergedResult.data).toStrictEqual([0, 2, 4, 6, 8]); | ||
expect(mergedResult.status).toStrictEqual('error'); | ||
}); | ||
}); | ||
|
||
it('should fetch more data when fetchNextPage is called', async () => { | ||
const { result } = renderHook(() => | ||
useMergedInfiniteQueries({ | ||
queries: MOCK_QUERY_CONFIG, | ||
pageSize: 5, | ||
flattenResponse: (res) => res.entries, | ||
compare: (a, b) => (a < b ? -1 : 1), | ||
}) | ||
); | ||
|
||
await waitFor(() => { | ||
const [_, queryResults] = result.current; | ||
expect(queryResults[0].data?.pages[0].entries).toStrictEqual([ | ||
0, 2, 4, 6, 8, | ||
]); | ||
expect(queryResults[1].data?.pages[0].entries).toStrictEqual([ | ||
1, 3, 5, 7, 9, | ||
]); | ||
}); | ||
|
||
const [{ fetchNextPage }] = result.current; | ||
|
||
act(() => { | ||
fetchNextPage(); | ||
}); | ||
|
||
await waitFor(() => { | ||
const [_, queryResults] = result.current; | ||
expect(queryResults[0].data?.pages[1].entries).toStrictEqual([ | ||
10, 12, 14, 16, 18, | ||
]); | ||
expect(queryResults[1].data?.pages[1].entries).toStrictEqual([ | ||
11, 13, 15, 17, 19, | ||
]); | ||
}); | ||
}); | ||
|
||
it('should skip fetching more data if enough data has already been fetched', async () => { | ||
const { result } = renderHook(() => | ||
useMergedInfiniteQueries({ | ||
queries: MOCK_QUERY_CONFIG, | ||
pageSize: 5, | ||
flattenResponse: (res) => res.entries, | ||
compare: (a, b) => (a < b ? -1 : 1), | ||
}) | ||
); | ||
|
||
await waitFor(() => { | ||
const [mergedResult, queryResults] = result.current; | ||
expect(mergedResult.status).toBe('success'); | ||
// 1 page fetched from each endpoint | ||
expect(queryResults[0].data?.pages.length).toStrictEqual(1); | ||
expect(queryResults[1].data?.pages.length).toStrictEqual(1); | ||
}); | ||
|
||
const [{ fetchNextPage: fetchSecondPage }] = result.current; | ||
|
||
act(() => { | ||
fetchSecondPage(); | ||
}); | ||
|
||
await waitFor(() => { | ||
const [mergedResult, queryResults] = result.current; | ||
expect(mergedResult.status).toBe('success'); | ||
// 2 pages fetched from each endpoint | ||
expect(queryResults[0].data?.pages.length).toStrictEqual(2); | ||
expect(queryResults[1].data?.pages.length).toStrictEqual(2); | ||
}); | ||
|
||
const [{ fetchNextPage: fetchThirdPage }] = result.current; | ||
|
||
act(() => { | ||
fetchThirdPage(); | ||
}); | ||
|
||
await waitFor(() => { | ||
const [mergedResult, queryResults] = result.current; | ||
expect(mergedResult.status).toBe('success'); | ||
// still just 2 pages fetched from each endpoint | ||
expect(queryResults[0].data?.pages.length).toStrictEqual(2); | ||
expect(queryResults[1].data?.pages.length).toStrictEqual(2); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 30 additions & 2 deletions
32
src/hooks/use-merged-infinite-queries/use-merged-infinite-queries.types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,39 @@ | ||
import { | ||
type QueryKey, | ||
type InfiniteData, | ||
type UseInfiniteQueryOptions, | ||
type useInfiniteQuery, | ||
} from '@tanstack/react-query'; | ||
|
||
export type MergedQueryStatus = 'idle' | 'loading' | 'success' | 'error'; | ||
|
||
export type MergedQueriesResults<T> = { | ||
data: Array<T>; | ||
export type MergedQueriesResults<TData> = { | ||
data: Array<TData>; | ||
status: MergedQueryStatus; | ||
isLoading: boolean; | ||
isFetching: boolean; | ||
isFetchingNextPage: boolean; | ||
hasNextPage: boolean; | ||
fetchNextPage: () => void; | ||
}; | ||
|
||
export type SingleInfiniteQueryOptions<TResponse, TPageParam> = | ||
UseInfiniteQueryOptions< | ||
TResponse, | ||
Error, | ||
InfiniteData<TResponse, TPageParam>, | ||
TResponse, | ||
QueryKey, | ||
TPageParam | ||
>; | ||
|
||
export type SingleInfiniteQueryResult<TResponse> = ReturnType< | ||
typeof useInfiniteQuery<TResponse> | ||
>; | ||
|
||
export type Props<TData, TResponse, TPageParam> = { | ||
queries: Array<SingleInfiniteQueryOptions<TResponse, TPageParam>>; | ||
pageSize: number; | ||
flattenResponse: (queryResult: TResponse) => Array<TData>; | ||
compare: (a: TData, b: TData) => number; | ||
}; |
Oops, something went wrong.