分页
请更新到最新版本(≥ 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
: 与useSWR
的error
相同。isLoading
: 与useSWR
的isLoading
相同。isValidating
: 与useSWR
的isValidating
相同。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
函数是 useSWRInfinite
和 useSWR
之间的主要区别。它接受当前页面的索引以及前一页的数据。因此,它可以很好地支持基于索引和基于游标的分页 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
- 如果已到达末尾,则禁用“加载更多”按钮
- 可更改的数据源
- 刷新整个列表