什么是 SWR?
SWR (stale-while-revalidate) 是由 Vercel 团队开发的数据获取 React Hooks 库。它的名字来源于 HTTP RFC 5861 中的 stale-while-revalidate
缓存策略:先返回缓存数据(stale),然后发送请求获取最新数据(revalidate)。
SWR 的核心特性
1. 自动数据缓存和重新验证
import useSWR from 'swr'
function Profile() {
const { data, error } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
SWR 会自动缓存数据,当你重新访问组件时可以立即显示缓存数据,同时在后台重新请求以确保数据是最新的。
2. 自动重新请求
SWR 提供了多种重新请求策略:
• 窗口获得焦点时重新请求
• 网络恢复时重新请求
• 定时轮询
const { data } = useSWR('/api/todos', fetcher, {
refreshInterval: 3000, // 每3秒重新请求一次
revalidateOnFocus: true, // 窗口聚焦时重新请求
revalidateOnReconnect: true // 网络重连时重新请求
})
3. 请求去重和缓存共享
多个组件请求相同的数据时,SWR 会自动去重并共享缓存:
// 这两个组件会共享同一份数据和请求
function ComponentA() {
const { data } = useSWR('/api/user', fetcher)
return <div>{data.name}</div>
}
function ComponentB() {
const { data } = useSWR('/api/user', fetcher)
return <div>{data.email}</div>
}
4. 乐观更新
SWR 支持在本地立即更新数据,同时在后台发送请求:
import { mutate } from 'swr'
// 立即更新本地数据
mutate('/api/todos', [...todos, newTodo], false)
// 发送请求
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo)
})
// 重新验证
mutate('/api/todos')
5. 分页和无限加载
SWR 提供了专门的 Hook 用于处理分页数据:
function Posts() {
const { data, size, setSize } = useSWRInfinite(
(index) => `/api/posts?page=${index + 1}`,
fetcher
)
return (
<div>
{data.map((posts, index) => (
<div key={index}>
{posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
<button onClick={() => setSize(size + 1)}>加载更多</button>
</div>
)
}
在 Next.js 14 中的最佳实践
1. 全局配置
// app/swr-provider.tsx
'use client'
import { SWRConfig } from 'swr'
export function SWRProvider({ children }) {
return (
<SWRConfig
value={{
fetcher: (url) => fetch(url).then(res => res.json()),
revalidateOnFocus: false,
revalidateOnReconnect: false
}}
>
{children}
</SWRConfig>
)
}
2. 配合 App Router 使用
Next.js 14 的 App Router 支持服务端组件,我们可以结合 SWR 实现最佳性能:
// app/page.tsx
async function Page() {
// 服务端获取初始数据
const initialData = await fetch('/api/data')
return (
<ClientComponent fallback={initialData} />
)
}
// components/client.tsx
'use client'
function ClientComponent({ fallback }) {
const { data } = useSWR('/api/data', { fallbackData: fallback })
return <div>{data}</div>
}
3. 错误处理和重试
function Profile() {
const { data, error, isLoading, isValidating, mutate } = useSWR(
'/api/user',
fetcher,
{
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// 超过 10 次后停止重试
if (retryCount >= 10) return
// 5xx 错误才重试
if (error.status >= 500) {
setTimeout(() => revalidate({ retryCount }), 5000)
}
}
}
)
if (error) {
return (
<div>
加载失败
<button onClick={() => mutate()}>重试</button>
</div>
)
}
return <div>{isValidating ? '刷新中...' : data}</div>
}
进阶使用技巧
1. 条件请求
// 当 user 存在时才请求
const { data: orders } = useSWR(
user ? `/api/orders?uid=${user.id}` : null,
fetcher
)
2. 依赖请求
// 第二个请求依赖第一个请求的结果
const { data: user } = useSWR('/api/user', fetcher)
const { data: orders } = useSWR(
() => user ? `/api/orders?uid=${user.id}` : null,
fetcher
)
3. 预请求
import { preload } from 'swr'
// 提前加载数据
preload('/api/posts', fetcher)
function Posts() {
// 数据可能已经在缓存中
const { data } = useSWR('/api/posts', fetcher)
return <div>{data}</div>
}
使用 SWR 构建现代博客系统
一、用户认证系统实现
1. 登录状态管理
首先,我们实现一个全局的用户状态管理hook:
// hooks/useAuth.ts
import useSWR from 'swr'
import { useRouter } from 'next/navigation'
interface User {
id: string
username: string
email: string
}
export function useAuth() {
const router = useRouter()
const { data: user, error, mutate } = useSWR<User>('/api/user', async (url) => {
const res = await fetch(url)
if (!res.ok) throw new Error('Failed to fetch user')
return res.json()
}, {
revalidateOnFocus: true, // 窗口聚焦时重新验证用户状态
dedupingInterval: 2000 // 2秒内重复请求会使用缓存
})
const login = async (email: string, password: string) => {
try {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
})
if (!res.ok) throw new Error('Login failed')
// 登录成功后立即更新用户数据
await mutate()
router.push('/dashboard')
} catch (error) {
throw error
}
}
const logout = async () => {
await fetch('/api/logout', { method: 'POST' })
// 退出后清空用户数据
await mutate(null)
router.push('/login')
}
return {
user,
loading: !error && !user,
error,
login,
logout
}
}
2. 登录组件实现
// components/LoginForm.tsx
'use client'
import { useState } from 'react'
import { useAuth } from '@/hooks/useAuth'
export function LoginForm() {
const { login, loading } = useAuth()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
await login(email, password)
} catch (error) {
alert('登录失败')
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="密码"
/>
<button type="submit" disabled={loading}>
{loading ? '登录中...' : '登录'}
</button>
</form>
)
}
二、博客系统实现
1. 文章列表与分页
// hooks/useArticles.ts
import useSWRInfinite from 'swr/infinite'
interface Article {
id: string
title: string
content: string
createdAt: string
author: {
name: string
avatar: string
}
}
export function useArticles(pageSize = 10) {
const getKey = (pageIndex: number) =>
`/api/articles?page=${pageIndex + 1}&limit=${pageSize}`
const {
data,
error,
size,
setSize,
mutate,
isValidating
} = useSWRInfinite<Article[]>(
getKey,
async (url) => {
const res = await fetch(url)
return res.json()
},
{
revalidateFirstPage: false,
persistSize: true
}
)
const articles = data ? data.flat() : []
const isLoadingInitialData = !data && !error
const isLoadingMore = size > 0 && data && typeof data[size - 1] === "undefined"
const isEmpty = data?.[0]?.length === 0
const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < pageSize)
return {
articles,
error,
isLoadingInitialData,
isLoadingMore,
isReachingEnd,
loadMore: () => setSize(size + 1),
mutate
}
}
2. 文章详情与实时更新
// hooks/useArticle.ts
import useSWR from 'swr'
export function useArticle(articleId: string) {
const { data: article, error, mutate } = useSWR(
`/api/articles/${articleId}`,
async (url) => {
const res = await fetch(url)
if (!res.ok) throw new Error('Failed to fetch article')
return res.json()
},
{
revalidateOnFocus: false,
dedupingInterval: 5000
}
)
const updateLikes = async () => {
// 乐观更新
await mutate(
{ ...article, likes: article.likes + 1 },
false
)
// 发送实际请求
await fetch(`/api/articles/${articleId}/like`, {
method: 'POST'
})
// 重新验证数据
await mutate()
}
return {
article,
error,
updateLikes
}
}
3. 博客列表组件
// components/ArticleList.tsx
'use client'
import { useArticles } from '@/hooks/useArticles'
export function ArticleList() {
const {
articles,
error,
isLoadingInitialData,
isLoadingMore,
isReachingEnd,
loadMore
} = useArticles()
if (error) return <div>加载失败</div>
if (isLoadingInitialData) return <div>加载中...</div>
return (
<div>
{articles.map(article => (
<article key={article.id}>
<h2>{article.title}</h2>
<div>作者: {article.author.name}</div>
<p>{article.content}</p>
</article>
))}
{!isReachingEnd && (
<button
onClick={loadMore}
disabled={isLoadingMore}
>
{isLoadingMore ? '加载中...' : '加载更多'}
</button>
)}
</div>
)
}
三、高级特性应用
1. 预加载与数据预取
// utils/prefetch.ts
import { preload } from 'swr'
export function prefetchArticle(articleId: string) {
preload(`/api/articles/${articleId}`, async (url) => {
const res = await fetch(url)
return res.json()
})
}
// 在文章列表中预加载
function ArticlePreview({ article }) {
return (
<div onMouseEnter={() => prefetchArticle(article.id)}>
<h3>{article.title}</h3>
</div>
)
}
2. 全局配置与错误处理
// app/swr-provider.tsx
'use client'
import { SWRConfig } from 'swr'
export function SWRProvider({ children }) {
return (
<SWRConfig
value={{
// 全局 fetcher
fetcher: async (url: string) => {
const res = await fetch(url)
if (!res.ok) {
const error = new Error('API 请求失败')
error.info = await res.json()
error.status = res.status
throw error
}
return res.json()
},
// 全局错误重试配置
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// 401 错误不重试
if (error.status === 401) return
// 最多重试 3 次
if (retryCount >= 3) return
// 5 秒后重试
setTimeout(() => revalidate({ retryCount }), 5000)
}
}}
>
{children}
</SWRConfig>
)
}
3. 条件请求与数据依赖
// hooks/useComments.ts
export function useComments(articleId: string) {
const { data: article } = useSWR(`/api/articles/${articleId}`)
// 只有文章存在且允许评论时才获取评论
const { data: comments } = useSWR(
() => article && article.allowComments
? `/api/articles/${articleId}/comments`
: null
)
return { comments }
}
四、性能优化实践
1. 合理使用缓存策略
const { data } = useSWR('/api/articles', {
// 静态内容可以缓存更久
dedupingInterval: 3600000, // 1小时
// 降低重新验证频率
refreshInterval: 0,
revalidateOnFocus: false
})
1. 优化数据粒度
// 分离数据请求,避免不必要的重新渲染
function ArticlePage() {
// 文章基础信息
const { data: article } = useSWR(`/api/articles/${id}/basic`)
// 文章统计信息
const { data: stats } = useSWR(`/api/articles/${id}/stats`, {
refreshInterval: 5000 // 频繁更新
})
// 文章内容
const { data: content } = useSWR(`/api/articles/${id}/content`, {
revalidateOnFocus: false // 内容不常变化
})
}
通过这个完整的博客系统案例,我们可以看到 SWR 在实际应用中的强大能力:
1. 简化了数据获取和状态管理
2. 提供了优秀的缓存策略
3. 支持复杂的数据依赖关系
4. 内置了性能优化机制
在 Next.js 14 中使用 SWR,可以充分利用服务端组件和客户端缓存,打造性能优秀的现代 Web 应用。希望这篇文章能帮助你更好地理解和使用 SWR!
更多关于 Next.js的全栈内容请点击下面的合集: