Skip to content

Instantly share code, notes, and snippets.

@nandorojo
Last active July 10, 2023 11:01
Show Gist options
  • 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
@bryanltobing
Copy link

do you have an example on how to call this using axios?

@nandorojo
Copy link
Author

Use it just like useSWRInfinite from SWR. It just comes with extra features.

https://swr.vercel.app/docs/pagination

@nandorojo
Copy link
Author

It’s a bit outdated though I’ll update when I have time

@bryanltobing
Copy link

have a hard time to figure this out using axios and type safety with typescript.

@JeromeFitz
Copy link

🍬 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:

import { ConfigInterface, useSWRInfinite } from 'swr'

Add:

import type { SWRInfiniteConfiguration } from 'swr/infinite'
import useSWRInfinite from 'swr/infinite'

types

Remove:

type SWRInfiniteConfigInterface ...

Edit:

export type UseGetInfinitePagesConfig<
  Page extends object
> = SWRInfiniteConfiguration<Page> & {
  limit?: number
  dataPath: keyof Page | string[]
}

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.

@dat23022000
Copy link

execute me, please show me example coding ..

@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