跳到内容
文档
分页

分页

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

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

何时使用 useSWR

分页

首先,我们可能 **不需要** useSWRInfinite,而可以使用 useSWR,如果我们正在构建类似这样的东西

…这是一个典型的分页 UI。让我们看看如何使用 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>
}

此外,我们可以为这个“页面组件”创建一个抽象。

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 将触发下一页的数据获取。当用户导航到下一页时,数据已经在那里。

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 钩子非常强大,可以覆盖大多数场景。

无限加载

有时我们想要构建一个 **无限加载** UI,带有一个“加载更多”按钮,它会将数据追加到列表中(或者当您滚动时自动完成)。

为了实现这一点,我们需要在这个页面上进行 **动态数量的请求**。React 钩子有 一些规则 (在新标签页中打开),所以我们 **不能** 做这样的事情。

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 /> 抽象来实现它。

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

高级案例

但是,在一些高级用例中,上面的解决方案不起作用。

例如,我们仍在实现相同的“加载更多”UI,但还需要显示一个数字来表示总共有多少项。我们不能再使用 <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 是 **基于游标的**,那么该解决方案也不起作用。因为每个页面都需要来自前一页的数据,所以它们不是独立的。

这就是这个新的 useSWRInfinite 钩子可以提供帮助。

useSWRInfinite

useSWRInfinite 使我们能够使用一个钩子触发多个请求。它看起来像这样

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

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

在无限加载中,一个*页面*就是一个请求,我们的目标是获取多个页面并呈现它们。

⚠️

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

API

参数

  • getKey: 一个接受索引和前一页数据的函数,返回一个页面的键。
  • fetcher: 与 useSWR获取器函数 相同。
  • options: 接受 useSWR 支持的所有选项,以及 4 个额外选项。
    • initialSize = 1: 最初应该加载的页面数。
    • revalidateAll = false: 始终尝试重新验证所有页面。
    • revalidateFirstPage = true: 始终尝试重新验证第一页。
    • persistSize = false: 当第一页的键发生变化时,不要将页面大小重置为 1(或如果设置了 initialSize 则重置为 initialSize)。
    • parallel = false: 并行获取多个页面。
💡

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

返回值

  • data: 每个页面的获取响应值的数组。
  • error: 与 useSWRerror 相同。
  • isLoading: 与 useSWRisLoading 相同。
  • isValidating: 与 useSWRisValidating 相同。
  • mutate: 与 useSWR 的绑定 mutate 函数相同,但会操作数据数组。
  • size: 将要获取和返回的页面数量。
  • setSize: 设置需要获取的页面数量。

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

对于普通的基于索引的 API。

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。

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

// `data` will look like this
[
  [
    { 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) => {
  // 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`
}

并行获取模式

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

useSWRInfinite 的默认行为是按顺序获取每个页面的数据,因为键的创建基于之前获取的数据。但是,如果页面之间没有依赖关系,则顺序获取大量页面的数据可能不是最佳的。通过将 parallel 选项指定为 true 将使您可以独立地并行获取页面,这可以显著加快加载过程。

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

重新验证特定页面

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

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

revalidate 函数将为每个页面调用。

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 将所有页面数据存储到缓存中,并使用特殊的缓存键以及每个页面数据,因此您必须在 swr/infinite 中使用 unstable_serialize 使用全局变异重新验证数据。

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

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

高级功能

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

  • 加载状态
  • 如果为空,则显示特殊的 UI
  • 如果已到达末尾,则禁用“加载更多”按钮
  • 可更改的数据源
  • 刷新整个列表