Skip to content
文档
分页

分页

请更新到最新版本 (≥ 0.3.0) 来用此 API。原来的 useSWRPages API 已废弃。

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

何时应该继续使用 useSWR

分页

首先,我们可能 并不 需要使用 useSWRInfinite,而是直接使用 useSWR,例如我们正在构建以下场景:

...这是一个典型的分页用户界面。来看看它如何用 useSWR 轻松实现:

function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  // API URL 中包含了页面索引,它是一个 React state。
  const { data } = useSWR(`/api/data?page=${pageIndex}`, fetcher);
 
  // ... 处理加载和错误的状态
 
  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>
}

我们还可以为这个页面组件创建一个抽象层。:

function Page ({ index }) {
  const { data } = useSWR(`/api/data?page=${index}`, fetcher);
 
  // ... 处理加载和错误状态
 
  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 会触发下一页页面的数据获取。当用户导航到下一页时,数据就已经存在了:

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

仅用一行代码,我们就获得了更好的用户体验。useSWR hook 已经非常强大了,能够覆盖大多数的场景。

无限加载

有时我们想构建一个无限加载的界面,通过一个 "Load More" 按钮向列表追加数据(或者当你滚动时自动加载):

要实现这个功能,我们需要在该页面上进行动态的数据请求。React Hooks 中存在这 两个规则 (opens in a new tab),所以我们 不能 这么写:

function App () {
  const [cnt, setCnt] = useState(1)
 
  const list = []
  for (let i = 0; i < cnt; i++) {
    // 🚨 出错了!通常来说,你不能在循环里使用 hooks。
    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/> 抽象来实现这个功能:

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

高级用例

但是,在某些高级用例中,上述的解决方案并不适用。

例如,我们仍然实现相同的 "Load More" 界面,但还需要显示一共有多少项。我们无法再使用 <Page /> 的解决方案,因为顶层 UI (<App/>) 需要每个页面中的数据。

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是 **基于游标(cursor)**的,那么这个解决方案也不适用。因为每个页面都需要前一页的数据,它们不是隔离的。

这意味着我们需要一个更高级的解决方案来解决这个问题,因此,我们引入了一个新的 useSWRInfinite Hook。。

useSWRInfinite

useSWRInfinite 让我们能够通过一个 Hook 触发多个请求。就像下面这样:

import useSWRInfinite from 'swr/infinite'
 
// ...
const { data, error, isLoading, isValidating, mutate, size, setSize } = useSWRInfinite(
  getKey, fetcher?, options?
)

与 useSWR 类似,这个新 Hook 接受一个返回请求 key 的函数、一个 fetcher 函数和一些选项。它返回和 useSWR 一样的所有值,并增加了两个额外的值:页面大小和一个页面大小的 setter,类似于 React 的 state。

在无限滚动中,一个 “页面” 就是一个请求,我们的目标是获取多个页面并将它们渲染出来。

⚠️

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

API

参数

  • getKey: 一个接受索引值和上一页数据,并返回页面 key 值的函数
  • fetcher: 和 useSWRfetcher 函数 一样
  • options: 接受 useSWR 支持的所有选项,以及四个额外选项:
    • initialSize = 1: 最初应加载的页面数量
    • revalidateAll = false: 始终尝试重新验证所有页面
    • revalidateFirstPage = true: 始终尝试重新验证第一页
    • persistSize = false: 当第一页的 key 发生变化时,不将 page size(或者 initialSize 如果设置了该参数)重置为 1
    • parallel = false: fetches multiple pages in parallel
💡

请注意,initialSize 选项不允许在运行时更改。

返回值

  • data: 由每一页的请求的响应数据组成的数组
  • error: 与 useSWRerror 返回值相同
  • isLoading: 与 useSWRisLoading 返回值相同
  • isValidating: 与 useSWRisValidating 返回值相同
  • mutate: 和 useSWR 的绑定 mutate 函数一样,但可以用于操作 data 数组
  • size: 即将请求并返回的页面数量
  • setSize: 设置需要被请求的页面数量

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

普通的基于索引的 API:

GET /users?page=0&limit=10
[
  { name: 'Alice', ... },
  { name: 'Bob', ... },
  { name: 'Cathy', ... },
  ...
]
// 一个用于拿到每个页面的 SWR key 的函数,
// 它的返回值会被 `fetcher` 接收。
// 如果返回值是 `null`,则该页面不会开始请求。
const getKey = (pageIndex, previousPageData) => {
  if (previousPageData && !previousPageData.length) return null // 已经到最后一页
  return `/users?page=${pageIndex}&limit=10`                    // SWR key
}
 
function App () {
  const { data, size, setSize } = useSWRInfinite(getKey, fetcher)
  if (!data) return 'loading'
 
  // 现在我们可以计算出用户的总数
  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` 是每个页面 API 响应的数组。
      return users.map(user => <div key={user.id}>{user.name}</div>)
    })}
    <button onClick={() => setSize(size + 1)}>Load More</button>
  </div>
}

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

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

// `data` 将如下所示
[
  [
    { name: 'Alice', ... },
    { name: 'Bob', ... },
    { name: 'Cathy', ... },
    ...
  ],
  [
    { name: 'John', ... },
    { name: 'Paul', ... },
    { name: 'George', ... },
    ...
  ],
  ...
]

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

假设现在 API 需要一个游标,并将下一个游标和数据一起返回:

GET /users?cursor=123&limit=10
{
  data: [
    { name: 'Alice' },
    { name: 'Bob' },
    { name: 'Cathy' },
    ...
  ],
  nextCursor: 456
}

我们可以将 getKey 函数改为下面这样:

const getKey = (pageIndex, previousPageData) => {
  // 已经到最后一页
  if (previousPageData && !previousPageData.data) return null
 
  // 在首页时,没有 `previousPageData`
  if (pageIndex === 0) return `/users?limit=10`
 
  // 将游标添加到 API
  return `/users?cursor=${previousPageData.nextCursor}&limit=10`
}

并行请求模式

请升级至最新版本(≥ 2.1.0)以使用此 API

useSWRInfinite 的默认行为是按顺序获取每个页面的数据,因为 key 的创建基于先前获取的数据。然而,对于一大堆没有相互依赖关系的页面,按顺序获取数据可能不是最优的,尤其是当页面数量很多时。通过将 parallel 选项设置为 true,你可以独立地并行获取页面数据,这可以显着加快加载过程。

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

当你启用了 parallel 选项,getKey 函数的参数 previousPageData 会变为 null

Revalidate Specific Pages

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

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.

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 数据

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.

useSWRInfinite 会将所有页面数据存储在缓存中,并使用特殊的缓存 key 来存储每个页面的数据。因此,您需要在 swr/infinite 中使用 unstable_serialize 对数据进行序列化,才能使用全局的 mutate 方法重新验证数据。

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

正如命名所示,unstable_serialize 目前并不是一个稳定的 API,所以我们可能会在未来修改它。

高级特性

这里有一个示例 演示了如何使用 useSWRInfinite 实现以下功能:

  • 显示加载状态
  • 如果为空,显示一个特殊的 UI
  • 如果加载到最后一页,禁用 "Load More" 按钮
  • 可变的数据源
  • 刷新整个列表