Pagination
Please update to the latest version (≥ 0.3.0) to use this API. The previous
useSWRPages
API is now deprecated.
SWR provides a dedicated API useSWRInfinite
to support common UI patterns such
as pagination and infinite loading.
When to Use useSWR
Pagination
First of all, we might NOT need useSWRInfinite
but can use just useSWR
if we are building something like this:
…which is a typical pagination UI. Let’s see how it can be easily implemented
with useSWR
:
function App() {
const [pageIndex, setPageIndex] = useState(0)
// The API URL includes the page index, which is a React state.
const { data } = useSWR(`/api/data?page=${pageIndex}`, fetcher)
// ... handle loading and error states
return (
<div>
{data.map(item => (
<div key={item.id}>{item.name}</div>
))}
<button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
<button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
</div>
)
}
Furthermore, we can create an abstraction for this “page component”:
function Page({ index }) {
const { data } = useSWR(`/api/data?page=${index}`, fetcher)
// ... handle loading and error states
return data.map(item => <div key={item.id}>{item.name}</div>)
}
function App() {
const [pageIndex, setPageIndex] = useState(0)
return (
<div>
<Page index={pageIndex} />
<button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
<button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
</div>
)
}
Because of SWR’s cache, we get the benefit to preload the next page. We render the next page inside a hidden div, so SWR will trigger the data fetching of the next page. When the user navigates to the next page, the data is already there:
function App() {
const [pageIndex, setPageIndex] = useState(0)
return (
<div>
<Page index={pageIndex} />
<div style={{ display: 'none' }}>
<Page index={pageIndex + 1} />
</div>
<button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
<button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
</div>
)
}
With just 1 line of code, we get a much better UX. The useSWR
hook is so
powerful, that most scenarios are covered by it.
Infinite Loading
Sometimes we want to build an infinite loading UI, with a “Load More” button that appends data to the list (or done automatically when you scroll):
To implement this, we need to make dynamic number of requests on this page. React Hooks have a couple of rules, so we CANNOT do something like this:
function App() {
const [cnt, setCnt] = useState(1)
const list = []
for (let i = 0; i < cnt; i++) {
// 🚨 This is wrong! Commonly, you can't use hooks inside a loop.
const { data } = useSWR(`/api/data?page=${i}`)
list.push(data)
}
return (
<div>
{list.map((data, i) => (
<div key={i}>
{data.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
))}
<button onClick={() => setCnt(cnt + 1)}>Load More</button>
</div>
)
}
Instead, we can use the <Page />
abstraction that we created to achieve it:
function App() {
const [cnt, setCnt] = useState(1)
const pages = []
for (let i = 0; i < cnt; i++) {
pages.push(<Page index={i} key={i} />)
}
return (
<div>
{pages}
<button onClick={() => setCnt(cnt + 1)}>Load More</button>
</div>
)
}
Advanced Cases
However, in some advanced use cases, the solution above doesn’t work.
For example, we are still implementing the same “Load More” UI, but also need to
display a number about how many items are there in total. We can’t use the
<Page />
solution anymore because the top level UI (<App />
) needs the data
inside each page:
function App() {
const [cnt, setCnt] = useState(1)
const pages = []
for (let i = 0; i < cnt; i++) {
pages.push(<Page index={i} key={i} />)
}
return (
<div>
<p>??? items</p>
{pages}
<button onClick={() => setCnt(cnt + 1)}>Load More</button>
</div>
)
}
Also, if the pagination API is cursor based, that solution doesn’t work either. Because each page needs the data from the previous page, they’re not isolated.
That’s how this new useSWRInfinite
Hook can help.
useSWRInfinite
useSWRInfinite
gives us the ability to trigger a number of requests with one
Hook. This is how it looks:
import useSWRInfinite from 'swr/infinite'
// ...
const { data, error, isValidating, mutate, size, setSize } = useSWRInfinite(
getKey, fetcher?, options?
)
Similar to useSWR
, this new Hook accepts a function that returns the request
key, a fetcher function, and options. It returns all the values that useSWR
returns, including 2 extra values: the page size and a page size setter, like a
React state.
In infinite loading, one page is one request, and our goal is to fetch multiple pages and render them.
If you are using SWR 0.x versions, useSWRInfinite
needs to be imported from
swr
:
import {useSWRInfinite} from 'swr'
API
Parameters
getKey
: a function that accepts the index and the previous page data, returns the key of a pagefetcher
: same asuseSWR
’s fetcher functionoptions
: accepts all the options thatuseSWR
supports, with 3 extra options:initialSize = 1
: number of pages should be loaded initiallyrevalidateAll = false
: always try to revalidate all pagesrevalidateFirstPage = true
: always try to revalidate the first pagepersistSize = false
: don’t reset the page size to 1 (orinitialSize
if set) when the first page’s key changes
Note that the initialSize
option is not allowed to change in the lifecycle.
Return Values
data
: an array of fetch response values of each pageerror
: same asuseSWR
’serror
isValidating
: same asuseSWR
’sisValidating
mutate
: same asuseSWR
’s bound mutate function but manipulates the data arraysize
: the number of pages that will be fetched and returnedsetSize
: set the number of pages that need to be fetched
Example 1: Index Based Paginated API
For normal index based APIs:
GET /users?page=0&limit=10
[
{ name: 'Alice', ... },
{ name: 'Bob', ... },
{ name: 'Cathy', ... },
...
]
// A function to get the SWR key of each page,
// its return value will be accepted by `fetcher`.
// If `null` is returned, the request of that page won't start.
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.length) return null // reached the end
return `/users?page=${pageIndex}&limit=10` // SWR key
}
function App() {
const { data, size, setSize } = useSWRInfinite(getKey, fetcher)
if (!data) return 'loading'
// We can now calculate the number of all users
let totalUsers = 0
for (let i = 0; i < data.length; i++) {
totalUsers += data[i].length
}
return (
<div>
<p>{totalUsers} users listed</p>
{data.map(users => {
// `data` is an array of each page's API response.
return users.map(user => <div key={user.id}>{user.name}</div>)
})}
<button onClick={() => setSize(size + 1)}>Load More</button>
</div>
)
}
The getKey
function is the major difference between useSWRInfinite
and
useSWR
. It accepts the index of the current page, as well as the data from the
previous page. So both index based and cursor based pagination API can be
supported nicely.
Also data
is no longer just one API response. It’s an array of multiple API
responses:
// `data` will look like this
[
[
{ name: 'Alice', ... },
{ name: 'Bob', ... },
{ name: 'Cathy', ... },
...
],
[
{ name: 'John', ... },
{ name: 'Paul', ... },
{ name: 'George', ... },
...
],
...
]
Example 2: Cursor or Offset Based Paginated API
Let’s say the API now requires a cursor and returns the next cursor alongside with the data:
GET /users?cursor=123&limit=10
{
data: [
{ name: 'Alice' },
{ name: 'Bob' },
{ name: 'Cathy' },
...
],
nextCursor: 456
}
We can change our getKey
function to:
const getKey = (pageIndex, previousPageData) => {
// reached the end
if (previousPageData && !previousPageData.data) return null
// first page, we don't have `previousPageData`
if (pageIndex === 0) return `/users?limit=10`
// add the cursor to the API endpoint
return `/users?cursor=${previousPageData.nextCursor}&limit=10`
}
Advanced Features
Here is an example showing how you can implement
the following features with useSWRInfinite
:
- loading states
- show a special UI if it’s empty
- disable the “Load More” button if reached the end
- changeable data source
- refresh the entire list