Last active
July 10, 2023 11:01
-
-
Save nandorojo/c93f00c2a378264addfea3777174ccfe to your computer and use it in GitHub Desktop.
useSWRInfinite with pagination & typescript safety
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
import { ConfigInterface, useSWRInfinite } from 'swr' | |
import { useMemo, useCallback, useRef } from 'react' | |
import last from 'lodash.last' | |
import get from 'lodash.get' | |
type PageKeyMaker<Page, Key extends any[]> = ( | |
index: number, | |
previousPageData?: Page | |
/** | |
* Mutable ref object. Set this to `true` before the request and `false` afterwards if the request is fetching more. | |
* | |
* For example, if the request has a `lastDocId`, it should set it to `true` before fetching. | |
* | |
* This prevents multiple page increases at once. | |
*/ | |
) => Key | |
type SWRInfiniteConfigInterface<Data = any, Error = any> = ConfigInterface< | |
Data[], | |
Error | |
> & { | |
initialSize?: number | |
revalidateAll?: boolean | |
persistSize?: boolean | |
} | |
export type UseGetInfinitePagesConfig< | |
Page extends object | |
> = SWRInfiniteConfigInterface<Page> & { | |
limit?: number | |
dataPath: keyof Page | string[] | |
} | |
type PageFetcher<Page, Key extends any[]> = ( | |
...params: Key | |
) => Page | Promise<Page> | |
const useGetInfinitePages = < | |
Page extends object, | |
Data, | |
/** | |
* Path to your list data | |
*/ | |
Key extends any[] = any[] | |
>( | |
key: PageKeyMaker<Page, Key>, | |
fetcher: PageFetcher<Page, Key>, | |
{ limit = 20, dataPath: path, ...options }: UseGetInfinitePagesConfig<Page> | |
) => { | |
const isFetching = useRef(false) | |
const dataPath = Array.isArray(path) ? path.join('.') : path | |
const { | |
data, | |
error, | |
isValidating, | |
mutate, | |
size, | |
setSize, | |
revalidate, | |
} = useSWRInfinite<Page>( | |
(index, previousPage) => { | |
const previousPageData = get(previousPage, dataPath) | |
// we've reached the last page, no more fetching | |
if (previousPageData?.length === 0) return null | |
// TODO is this correct? | |
// this means we haven't fetched the previous page yet, so don't fetch multiple at once. | |
// if (index > 0 && !previousPageData) return null | |
if (isFetching.current && index) return null | |
if (previousPageData && previousPageData.length < limit) { | |
return null | |
} | |
return key(index, previousPageData) | |
}, | |
async (...key: Key) => { | |
let val: Page | |
try { | |
isFetching.current = true | |
val = await fetcher(...key) | |
if (isFetching.current) { | |
isFetching.current = false | |
} | |
} catch (e) { | |
if (isFetching.current) { | |
isFetching.current = false | |
} | |
throw e | |
} | |
return val | |
}, | |
{ revalidateAll: false, ...options } | |
) | |
const firstPageData = get(data?.[0], dataPath) | |
const lastPage = last(data) | |
const lastPageData = get(lastPage, dataPath) | |
const canFetchMore = lastPageData?.length && lastPageData.length === limit | |
const isLoadingInitialData = !data && !error | |
const isLoadingMore = | |
isLoadingInitialData || | |
(isValidating && size > 1 && data && typeof data[size - 1] === 'undefined') | |
const isRefreshing = isValidating && data?.length === size | |
const isEmpty = firstPageData?.length === 0 | |
const fetchMore = useCallback(() => { | |
if (isLoadingMore || isFetching.current) return null | |
setSize((size) => { | |
console.log('π [use-get-infinite-pages] is fetching more', { | |
currentPage: size, | |
}) | |
return size + 1 | |
}) | |
}, [isLoadingMore, setSize]) | |
const flat = useMemo( | |
() => | |
data | |
?.map((page) => get(page, dataPath) as Data) | |
?.flat(1) | |
.filter(Boolean) as | |
| (Data extends readonly (infer InnerArr)[] ? InnerArr : Data)[] | |
| undefined, | |
[data, dataPath] | |
) | |
return { | |
data: flat, | |
pages: data, | |
error, | |
isValidating, | |
mutate, | |
fetchMore, | |
isFetchingMore: !!isLoadingMore, | |
isRefreshing, | |
isEmpty, | |
isLoadingInitialData, | |
isLoadingMore, | |
lastPage, | |
size, | |
revalidate, | |
canFetchMore, | |
} | |
} | |
export default useGetInfinitePages |
execute me, please show me example coding ..
Can you provide an code example?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
π¬ This is really sweet @nandorojo
π Thanks!
To upgrade to
[email protected]
(at time of writing it is at[email protected]
), I did the following:imports
Remove:
Add:
types
Remove:
Edit:
options defaults
revalidateAll: false
is out of the box (ref:swr
) so you could pass:{ revalidateAll: false, ...options }
options
However, I like π seeing π it too, and have added
revalidateFirstPage: false
on mine π@bryantobing12 If you have not seen the updated
swr
website there is an Axios section:To use with this gist, you should pass the
fetcher
to the hook.If there is more type safety required after you start hacking away, please share.