Skip to content
开发文档
分页

分页

¥Pagination

请更新至最新版本(≥0.3.0)才能使用此 API。以前的 useSWRPages API 现已弃用。

¥Please update to the latest version (≥ 0.3.0) to use this API. The previous useSWRPages API is now deprecated.

SWR 提供了专用的 API useSWRInfinite 来支持常见的 UI 模式,例如分页和无限加载。

¥SWR provides a dedicated API useSWRInfinite to support common UI patterns such as pagination and infinite loading.

何时使用 useSWR

¥When to Use useSWR

分页

¥Pagination

首先,如果我们构建如下内容,我们可能不需要 useSWRInfinite,但可以只使用 useSWR

¥First of all, we might NOT need useSWRInfinite but can use just useSWR if we are building something like this:

...这是一个典型的分页 UI。让我们看看如何使用 useSWR 轻松实现:

¥...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>
}

由于 SWR 的缓存,我们可以预加载下一页。我们在隐藏的 div 中渲染下一页,因此 SWR 将触发下一页的数据请求。当用户导航到下一页时,数据已经存在:

¥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>
}

只需 1 行代码,我们就可以获得更好的用户体验。useSWR 钩子太强大了,大部分场景都被它覆盖了。

¥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

有时我们想要构建一个无限加载的 UI,使用 "加载更多" 按钮将数据附加到列表(或在滚动时自动补齐):

¥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):

为了实现这一点,我们需要在此页面上发出动态数量的请求。React Hooks 有 一些规则 (opens in a new tab),所以我们不能做这样的事情:

¥To implement this, we need to make dynamic number of requests on this page. React Hooks have a couple of rules (opens in a new tab), 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>
}

相反,我们可以使用我们创建的 <Page /> 抽象来实现它:

¥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.

例如,我们仍然实现相同的 "加载更多" UI,但还需要显示有关总共有多少项的数字。我们不能再使用 <Page /> 解决方案,因为顶层 UI (<App />) 需要每个页面内的数据:

¥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>
}

另外,如果分页 API 是基于游标的,则该解决方案也不起作用。由于每个页面都需要前一页的数据,因此它们不是孤立的。

¥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.

这就是这款新型 useSWRInfinite Hook 的作用。

¥That's how this new useSWRInfinite Hook can help.

useSWRInfinite

useSWRInfinite 使我们能够用一个 Hook 触发多个请求。它看起来是这样的:

¥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, isLoading, isValidating, mutate, size, setSize } = useSWRInfinite(
  getKey, fetcher?, options?
)

useSWR 类似,这个新 Hook 接受一个返回请求键的函数、一个 fetcher 函数和选项。它返回 useSWR 返回的所有值,包括 2 个额外值:页面大小和页面大小设置器,如 React 状态。

¥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.

⚠️

如果你使用的是 SWR 0.x 版本,则需要从 swr:
import { useSWRInfinite } from 'swr' 导入 useSWRInfinite

¥If you are using SWR 0.x versions, useSWRInfinite needs to be imported from swr:
import { useSWRInfinite } from 'swr'

API

参数

¥Parameters

  • getKey:接受索引和上一页数据的函数,返回页面的键

    ¥getKey: a function that accepts the index and the previous page data, returns the key of a page

  • fetcher:与 useSWR获取函数 相同

    ¥fetcher: same as useSWR's fetcher function

  • options:接受 useSWR 支持的所有选项,还有 4 个额外选项:

    ¥options: accepts all the options that useSWR supports, with 4 extra options:

    • initialSize = 1:最初应加载的页数

      ¥initialSize = 1: number of pages should be loaded initially

    • revalidateAll = false:总是尝试重新验证所有页面

      ¥revalidateAll = false: always try to revalidate all pages

    • revalidateFirstPage = true:总是尝试重新验证第一页

      ¥revalidateFirstPage = true: always try to revalidate the first page

    • persistSize = false:当第一页的键更改时,不要将页面大小重置为 1(或 initialSize,如果设置)

      ¥persistSize = false: don't reset the page size to 1 (or initialSize if set) when the first page's key changes

    • parallel = false:并行获取多个页面

      ¥parallel = false: fetches multiple pages in parallel

💡

请注意,initialSize 选项在生命周期中不允许更改。

¥Note that the initialSize option is not allowed to change in the lifecycle.

返回值

¥Return Values

  • data:每个页面的获取响应值的数组

    ¥data: an array of fetch response values of each page

  • error:与 useSWRerror 相同

    ¥error: same as useSWR's error

  • isLoading:与 useSWRisLoading 相同

    ¥isLoading: same as useSWR's isLoading

  • isValidating:与 useSWRisValidating 相同

    ¥isValidating: same as useSWR's isValidating

  • mutate:与 useSWR 的绑定 mutate 函数相同,但操作数据数组

    ¥mutate: same as useSWR's bound mutate function but manipulates the data array

  • size:将获取和返回的页面数

    ¥size: the number of pages that will be fetched and returned

  • setSize:设置需要抓取的页数

    ¥setSize: set the number of pages that need to be fetched

示例 1:基于索引的分页 API

¥Example 1: Index Based Paginated API

对于普通的基于索引的 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, index) => {
      // `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>
}

getKey 功能是 useSWRInfiniteuseSWR 之间的主要区别。它接受当前页面的索引以及上一页的数据。因此基于索引和基于游标的分页 API 都可以得到很好的支持。

¥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.

此外,data 不再只是一个 API 响应。它是多个 API 响应的数组:

¥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', ... },
    ...
  ],
  ...
]

示例 2:基于光标或偏移量的分页 API

¥Example 2: Cursor or Offset Based Paginated API

假设 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
}

我们可以将 getKey 函数更改为:

¥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`
}

并行抓取模式

¥Parallel Fetching Mode

请更新至最新版本(≥2.1.0)才能使用此 API。

¥Please update to the latest version (≥ 2.1.0) to use this API.

useSWRInfinite 的默认行为是按顺序获取每个页面的数据,因为键创建基于之前获取的数据。然而,按顺序获取大量页面的数据可能不是最佳的,特别是在页面不相互依赖的情况下。通过为 true 指定 parallel 选项,你可以并行独立地获取页面,这可以显着加快加载过程。

¥The default behavior of useSWRInfinite is to fetch data for each page in sequence, as key creation is based on the previously fetched data. However, fetching data sequentially for a large number of pages may not be optimal, particularly if the pages are not interdependent. By specifying parallel option to true will let you fetch pages independently in parallel, which can significantly speed up the loading process.

// parallel = false (default)
// page1 ===> page2 ===> page3 ===> done
//
// parallel = true
// page1 ==> done
// page2 =====> done
// page3 ===> done
//
// previousPageData is always `null`
const getKey = (pageIndex, previousPageData) => {
  return `/users?page=${pageIndex}&limit=10`
}
 
function App () {
  const { data } = useSWRInfinite(getKey, fetcher, { parallel: true })
}
⚠️

当启用 parallel 选项时,getKey 函数的 previousPageData 参数将变为 null

¥The previousPageData argument of the getKey function becomes null when you enable the parallel option.

重新验证特定页面

¥Revalidate Specific Pages

请更新到最新版本 (≥ 2.2.5) 以使用此 API。

¥Please update to the latest version (≥ 2.2.5) to use this API.

useSWRInfinite 突变的默认行为是重新验证已加载的所有页面。但是你可能只想重新验证已更改的特定页面。你可以通过将函数传递给 revalidate 选项来仅重新验证特定页面。

¥The default behavior of the mutation of useSWRInfinite is to revalidate all pages that have been loaded. But you might want to revalidate only the specific pages that have been changed. You can revalidate only specific pages by passing a function to the revalidate option.

每个页面都会调用 revalidate 函数。

¥The revalidate function is called for each page.

function App() {
  const { data, mutate, size } = useSWRInfinite(
    (index) => [`/api/?page=${index + 1}`, index + 1],
    fetcher
  );
 
  mutate(data, {
    // only revalidate the last page
    revalidate: (pageData, [url, page]) => page === size
  });
}

使用 useSWRInfinite 进行全局修改

¥Global Mutate with useSWRInfinite

useSWRInfinite 使用特殊的缓存键将所有页面数据与每个页面数据一起存储到缓存中,因此你必须在 swr/infinite 中使用 unstable_serialize 通过全局修改重新验证数据。

¥useSWRInfinite stores all page data into the cache with a special cache key along with each page data, so you have to use unstable_serialize in swr/infinite to revalidate the data with the global mutate.

import { useSWRConfig } from "swr"
import { unstable_serialize } from "swr/infinite"
 
function App() {
    const { mutate } = useSWRConfig()
    mutate(unstable_serialize(getKey))
}
⚠️

顾名思义,unstable_serialize 不是一个稳定的 API,因此我们将来可能会更改它。

¥As the name implies, unstable_serialize is not a stable API, so we might change it in the future.

高级特性

¥Advanced Features

这是一个例子 展示了如何使用 useSWRInfinite 实现以下功能:

¥Here is an example showing how you can implement the following features with useSWRInfinite:

  • 加载状态

    ¥loading states

  • 如果它是空的,则显示一个特殊的 UI

    ¥show a special UI if it's empty

  • 如果到达终点则禁用 "加载更多" 按钮

    ¥disable the "Load More" button if reached the end

  • 可变数据源

    ¥changeable data source

  • 刷新整个列表

    ¥refresh the entire list