Skip to content

Instantly share code, notes, and snippets.

@nandorojo
Last active July 10, 2023 11:01
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nandorojo/c93f00c2a378264addfea3777174ccfe to your computer and use it in GitHub Desktop.
Save nandorojo/c93f00c2a378264addfea3777174ccfe to your computer and use it in GitHub Desktop.
useSWRInfinite with pagination & typescript safety
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
@yarinsa
Copy link

yarinsa commented Jul 10, 2023

Can you provide an code example?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment