TanStack Query vs SWR 深度对比与最佳实践
目录
- 概述
- 核心概念对比
- [TanStack Query 详解](#TanStack Query 详解 "#tanstack-query-%E8%AF%A6%E8%A7%A3")
- [TanStack Query 缓存机制](#TanStack Query 缓存机制 "#tanstack-query-%E7%BC%93%E5%AD%98%E6%9C%BA%E5%88%B6")
- [SWR 详解](#SWR 详解 "#swr-%E8%AF%A6%E8%A7%A3")
- [SWR 缓存机制](#SWR 缓存机制 "#swr-%E7%BC%93%E5%AD%98%E6%9C%BA%E5%88%B6")
- 性能对比分析
- 使用场景选择
- 最佳实践
- 迁移指南
概述
在现代 React 应用开发中,数据获取和状态管理是核心需求。TanStack Query(原 React Query)和 SWR 是两个最流行的数据获取库,它们都提供了缓存、重新验证、错误处理等功能,但在设计理念和使用方式上存在差异。
核心价值对比
特性 | TanStack Query | SWR |
---|---|---|
设计理念 | 功能丰富的数据管理平台 | 轻量级数据获取库 |
学习曲线 | 中等到复杂 | 简单到中等 |
包大小 | 较大 (~39KB) | 较小 (~24KB) |
生态系统 | 丰富的插件和工具 | 简洁的核心功能 |
TypeScript | 优秀的类型支持 | 良好的类型支持 |
核心概念对比
数据获取模式
TanStack Query 详解
基础使用
javascript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// 基础查询示例
function UserProfile({ userId }) {
const {
data: user,
isLoading,
error,
isError,
refetch
} = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('获取用户信息失败');
}
return response.json();
},
// 缓存时间
staleTime: 5 * 60 * 1000, // 5分钟
// 缓存垃圾回收时间
cacheTime: 10 * 60 * 1000, // 10分钟
// 重试配置
retry: (failureCount, error) => {
if (error.status === 404) return false;
return failureCount < 3;
},
// 重试延迟
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
});
if (isLoading) return <UserSkeleton />;
if (isError) return <ErrorMessage error={error} onRetry={refetch} />;
return (
<div className="user-profile">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
高级查询功能
javascript
// 并行查询
function Dashboard({ userId }) {
const userQuery = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
const postsQuery = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchUserPosts(userId),
// 依赖查询 - 等用户数据加载完成
enabled: !!userQuery.data
});
const statsQuery = useQuery({
queryKey: ['stats', userId],
queryFn: () => fetchUserStats(userId),
enabled: !!userQuery.data
});
const queries = [userQuery, postsQuery, statsQuery];
const isLoading = queries.some(query => query.isLoading);
const hasError = queries.some(query => query.isError);
if (isLoading) return <DashboardSkeleton />;
if (hasError) return <ErrorBoundary />;
return (
<div className="dashboard">
<UserInfo user={userQuery.data} />
<PostsList posts={postsQuery.data} />
<StatsWidget stats={statsQuery.data} />
</div>
);
}
// 无限查询
function PostsList() {
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam = 0 }) => {
const response = await fetch(`/api/posts?page=${pageParam}&limit=10`);
return response.json();
},
getNextPageParam: (lastPage, pages) => {
return lastPage.hasMore ? pages.length : undefined;
},
});
const posts = data?.pages.flatMap(page => page.posts) ?? [];
return (
<div className="posts-list">
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
className="load-more-btn"
>
{isFetchingNextPage ? '加载中...' : '加载更多'}
</button>
)}
{isFetching && !isFetchingNextPage && <LoadingSpinner />}
</div>
);
}
变更操作
javascript
// 数据变更和乐观更新
function PostEditor({ postId, onSuccess }) {
const queryClient = useQueryClient();
const { data: post } = useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId)
});
const updateMutation = useMutation({
mutationFn: async (updatedPost) => {
const response = await fetch(`/api/posts/${postId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedPost)
});
if (!response.ok) throw new Error('更新失败');
return response.json();
},
// 乐观更新
onMutate: async (newPost) => {
// 取消正在进行的查询
await queryClient.cancelQueries({ queryKey: ['post', postId] });
// 保存之前的数据用于回滚
const previousPost = queryClient.getQueryData(['post', postId]);
// 乐观更新
queryClient.setQueryData(['post', postId], {
...previousPost,
...newPost,
updatedAt: new Date().toISOString()
});
return { previousPost };
},
// 成功时更新相关查询
onSuccess: (data) => {
queryClient.setQueryData(['post', postId], data);
// 更新列表数据
queryClient.invalidateQueries({ queryKey: ['posts'] });
onSuccess?.(data);
},
// 错误时回滚
onError: (err, newPost, context) => {
if (context?.previousPost) {
queryClient.setQueryData(['post', postId], context.previousPost);
}
},
// 无论成功失败都重新获取数据
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['post', postId] });
},
});
const handleSubmit = (formData) => {
updateMutation.mutate(formData);
};
if (!post) return <PostSkeleton />;
return (
<PostForm
initialData={post}
onSubmit={handleSubmit}
isSubmitting={updateMutation.isPending}
error={updateMutation.error}
/>
);
}
高级配置和中间件
javascript
// 全局配置
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 全局查询配置
staleTime: 5 * 60 * 1000,
cacheTime: 10 * 60 * 1000,
retry: (failureCount, error) => {
// 全局重试逻辑
if (error.status >= 400 && error.status < 500) {
return false; // 客户端错误不重试
}
return failureCount < 3;
},
// 全局错误处理
onError: (error) => {
console.error('Query Error:', error);
if (error.status === 401) {
// 处理认证错误
window.location.href = '/login';
}
},
},
mutations: {
// 全局变更配置
onError: (error) => {
console.error('Mutation Error:', error);
// 显示错误提示
toast.error(error.message);
},
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<Router>
<Routes>
{/* 路由配置 */}
</Routes>
</Router>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
TanStack Query 缓存机制
TanStack Query 提供了强大而灵活的缓存系统,能够智能地管理数据的生命周期,提供优秀的用户体验。
能否判断是否是否新鲜?是否过期?
TanStack Query(React Query)之所以能判断缓存是否"新鲜"或"过期" ,是因为它对每一条缓存数据都维护了完整的生命周期元信息(metadata) ,并且采用了 时间驱动的缓存策略。
TanStack Query 会记录每条缓存的"最后更新时间",并对照配置项判断数据是否还"新鲜"或"过期"。
- 每条缓存都有精确的更新时间(
dataUpdatedAt
) - 配合用户设定的
staleTime
和cacheTime
- 系统自动判断缓存是否 stale 或 expired,决定是否重新 fetch 或垃圾回收
生命周期元信息
生命周期元信息(metadata),指的是它为每一个 query 自动维护的一组状态与时间戳,这些信息帮助它判断:
- 缓存是否**新鲜(fresh)*还是*陈旧(stale)
- 缓存是否应该重新请求(refetch)
- 缓存是否可以被自动清除(garbage collected)
- Query 当前状态是 loading、success、error,还是 idle
- 是否在被使用中(active)
生命周期元信息包含哪些?
每一个 Query
实例内部会维护类似以下结构:
ts
{
queryKey: ['todos'],
state: {
data, // 当前缓存的数据
dataUpdatedAt, // 上次 fetch 成功的时间戳(用于 stale 判断)
error, // 最近一次请求的错误
errorUpdatedAt, // 错误时间
fetchStatus, // 'idle' | 'fetching' | 'paused'
status, // 'loading' | 'success' | 'error'
isInvalidated, // 是否被标记为"无效"------触发 refetch
isPaused, // 当前是否暂停请求
fetchMeta // 请求的一些附加信息,如 retry 次数等
},
observers: [...], // 哪些组件正在使用它(用于 active 判断)
gcTimeout, // GC 回收的定时器
options: { staleTime, cacheTime, ... }
}
关键字段说明
字段名 | 含义 |
---|---|
dataUpdatedAt |
上次请求成功的时间戳,用于判断是否 stale |
status |
当前 query 状态:loading / success / error |
fetchStatus |
是否正在请求中:fetching / idle |
isInvalidated |
被标记为"无效",会触发下次自动刷新 |
observers |
有多少组件在"使用"这个 query,决定是否需要 GC |
gcTimeout |
缓存未使用后触发的垃圾回收计时器 |
生命周期流程简图
应用场景:为什么需要这些 metadata?
自动刷新数据的时机判断
ts
if (Date.now() - dataUpdatedAt > staleTime) {
// 数据是 stale 的,触发 refetch
}
判断是否有组件在用(决定是否 GC)
ts
if (query.observers.length === 0) {
// 启动 GC 计时器,准备删除缓存
}
控制 UI 状态反馈
ts
if (status === 'loading') showSpinner()
if (status === 'error') showError()
想看 metadata?React Query Devtools!
如果你使用 React Query Devtools,可以看到每个 query 的元信息:
- Data
- isStale
- UpdatedAt 时间
- 状态字段
- 是否 active
- 是否正在 fetching
示例图:
yaml
Query ['todos']
- Status: success
- isStale: true
- Data updated at: 12:03:12 PM
- Active observers: 1
- Fetching: false
TanStack Query 的 生命周期元信息(metadata) 是一整套用于管理缓存生命周期的状态和时间戳数据,它支撑了 query 的核心功能,包括:
- 是否需要重新请求?
- 数据是否还新鲜?
- 缓存是否可以清除?
- UI 如何正确反馈加载、错误状态?
这些 metadata 让 TanStack Query 成为"智能数据管理库"而不仅仅是"数据请求库"。
缓存生命周期
缓存状态管理
javascript
// 缓存状态示例
function CacheStatusDemo() {
const queryClient = useQueryClient();
const { data, status, fetchStatus, dataUpdatedAt } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 5 * 60 * 1000, // 5分钟内认为数据新鲜
cacheTime: 30 * 60 * 1000, // 30分钟后垃圾回收
});
// 查询状态详解
const queryState = {
// status: 查询的状态
// 'pending' - 首次加载中
// 'error' - 查询失败
// 'success' - 查询成功
status,
// fetchStatus: 网络请求状态
// 'idle' - 没有网络请求
// 'fetching' - 正在请求
fetchStatus,
// 数据相关
hasData: !!data,
dataUpdatedAt: new Date(dataUpdatedAt).toLocaleString(),
};
return (
<div className="cache-demo">
<h3>缓存状态监控</h3>
<pre>{JSON.stringify(queryState, null, 2)}</pre>
<div className="actions">
<button onClick={() => queryClient.invalidateQueries({ queryKey: ['posts'] })}>
使缓存失效
</button>
<button onClick={() => queryClient.removeQueries({ queryKey: ['posts'] })}>
清除缓存
</button>
<button onClick={() => queryClient.resetQueries({ queryKey: ['posts'] })}>
重置查询
</button>
</div>
</div>
);
}
缓存配置详解
javascript
// 详细的缓存配置
const cacheConfig = {
// 数据新鲜度配置
staleTime: 5 * 60 * 1000, // 5分钟内数据被认为是新鲜的
// 缓存时间配置
cacheTime: 30 * 60 * 1000, // 数据在内存中保存30分钟
// 重新获取策略
refetchOnMount: true, // 组件挂载时重新获取
refetchOnWindowFocus: true, // 窗口获得焦点时重新获取
refetchOnReconnect: true, // 网络重连时重新获取
// 重试配置
retry: (failureCount, error) => {
if (error.status === 404) return false;
return failureCount < 3;
},
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
};
// 应用缓存配置
function PostsWithCache() {
const { data: posts, isStale, isFetching } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
...cacheConfig,
});
return (
<div className="posts-container">
<div className="cache-indicators">
{isStale && <span className="stale-badge">数据已过期</span>}
{isFetching && <span className="fetching-badge">更新中...</span>}
</div>
<PostsList posts={posts} />
</div>
);
}
缓存更新策略
javascript
// 手动缓存更新
function CacheUpdateStrategies() {
const queryClient = useQueryClient();
// 策略1: 直接设置缓存数据
const setUserData = (userId, userData) => {
queryClient.setQueryData(['user', userId], userData);
};
// 策略2: 函数式更新
const updateUserLikes = (userId) => {
queryClient.setQueryData(['user', userId], (oldData) => {
if (!oldData) return oldData;
return {
...oldData,
likes: oldData.likes + 1,
updatedAt: new Date().toISOString(),
};
});
};
// 策略3: 批量更新相关缓存
const updateUserAndPosts = async (userId, newUserData) => {
// 更新用户数据
queryClient.setQueryData(['user', userId], newUserData);
// 更新相关的文章列表缓存
queryClient.setQueryData(['posts', 'user', userId], (oldPosts) => {
if (!oldPosts) return oldPosts;
return oldPosts.map(post => ({
...post,
author: newUserData,
}));
});
// 使相关查询失效
queryClient.invalidateQueries({ queryKey: ['posts', 'feed'] });
};
// 策略4: 条件性缓存更新
const smartCacheUpdate = (data) => {
queryClient.setQueriesData(
{ queryKey: ['posts'] },
(oldData) => {
// 只更新包含特定条件的缓存
if (oldData?.meta?.category === data.category) {
return {
...oldData,
posts: [...oldData.posts, data],
};
}
return oldData;
}
);
};
return {
setUserData,
updateUserLikes,
updateUserAndPosts,
smartCacheUpdate,
};
}
缓存预取和预填充
javascript
// 缓存预取策略
function CachePrefetching() {
const queryClient = useQueryClient();
// 预取下一页数据
const prefetchNextPage = async (currentPage) => {
const nextPage = currentPage + 1;
// 预取下一页,但不会触发加载状态
await queryClient.prefetchQuery({
queryKey: ['posts', 'page', nextPage],
queryFn: () => fetchPosts({ page: nextPage }),
staleTime: 5 * 60 * 1000,
});
};
// 鼠标悬停时预取详情
const prefetchPostDetail = async (postId) => {
await queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
staleTime: 10 * 60 * 1000,
});
};
// 预填充相关数据
const prefillUserData = (users) => {
users.forEach(user => {
queryClient.setQueryData(['user', user.id], user);
});
};
return (
<div className="posts-list">
{posts.map((post, index) => (
<div
key={post.id}
onMouseEnter={() => prefetchPostDetail(post.id)}
className="post-item"
>
<PostCard post={post} />
{index === posts.length - 5 && prefetchNextPage(currentPage)}
</div>
))}
</div>
);
}
缓存持久化
javascript
import { persistQueryClient } from '@tanstack/react-query-persist-client-core';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
// 配置持久化存储
const persister = createSyncStoragePersister({
storage: window.localStorage,
key: 'REACT_QUERY_OFFLINE_CACHE',
serialize: JSON.stringify,
deserialize: JSON.parse,
});
// 创建支持持久化的 QueryClient
const queryClient = new QueryClient({
defaultOptions: {
queries: {
cacheTime: 1000 * 60 * 60 * 24, // 24小时
staleTime: 1000 * 60 * 5, // 5分钟
},
},
});
// 初始化持久化
persistQueryClient({
queryClient,
persister,
maxAge: 1000 * 60 * 60 * 24, // 24小时
buster: '1.0', // 版本控制
});
// 选择性持久化
function SelectivePersistence() {
const { data } = useQuery({
queryKey: ['user-settings'],
queryFn: fetchUserSettings,
// 这个查询会被持久化
meta: {
persist: true,
},
});
const { data: tempData } = useQuery({
queryKey: ['temp-data'],
queryFn: fetchTempData,
// 这个查询不会被持久化
meta: {
persist: false,
},
});
return <div>{/* 组件内容 */}</div>;
}
缓存调试和监控
javascript
// 缓存调试工具
function CacheDebugger() {
const queryClient = useQueryClient();
const queryCache = queryClient.getQueryCache();
// 获取所有缓存状态
const getAllCacheInfo = () => {
const queries = queryCache.getAll();
return queries.map(query => ({
queryKey: query.queryKey,
state: query.state.status,
dataUpdatedAt: query.state.dataUpdatedAt,
errorUpdatedAt: query.state.errorUpdatedAt,
fetchStatus: query.state.fetchStatus,
isStale: query.isStale(),
observersCount: query.getObserversCount(),
}));
};
// 缓存性能监控
const monitorCachePerformance = () => {
const observer = {
onQueryAdded: (query) => {
console.log('Query Added:', query.queryKey);
},
onQueryRemoved: (query) => {
console.log('Query Removed:', query.queryKey);
},
onQueryUpdated: (query) => {
console.log('Query Updated:', query.queryKey, {
fetchStatus: query.state.fetchStatus,
status: query.state.status,
});
},
};
// 订阅缓存事件
const unsubscribe = queryCache.subscribe(observer);
return unsubscribe;
};
const [cacheInfo, setCacheInfo] = useState([]);
useEffect(() => {
const interval = setInterval(() => {
setCacheInfo(getAllCacheInfo());
}, 1000);
const unsubscribe = monitorCachePerformance();
return () => {
clearInterval(interval);
unsubscribe();
};
}, []);
return (
<div className="cache-debugger">
<h3>缓存监控面板</h3>
<table>
<thead>
<tr>
<th>Query Key</th>
<th>状态</th>
<th>最后更新</th>
<th>观察者数量</th>
<th>是否过期</th>
</tr>
</thead>
<tbody>
{cacheInfo.map((info, index) => (
<tr key={index}>
<td>{JSON.stringify(info.queryKey)}</td>
<td>{info.state}</td>
<td>{new Date(info.dataUpdatedAt).toLocaleTimeString()}</td>
<td>{info.observersCount}</td>
<td>{info.isStale ? '是' : '否'}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
SWR 详解
基础使用
javascript
import useSWR, { mutate } from 'swr';
// 全局 fetcher 函数
const fetcher = async (url) => {
const response = await fetch(url);
if (!response.ok) {
const error = new Error('请求失败');
error.status = response.status;
throw error;
}
return response.json();
};
// 基础用法
function UserProfile({ userId }) {
const {
data: user,
error,
isLoading,
mutate: refreshUser
} = useSWR(`/api/users/${userId}`, fetcher, {
// 重新验证策略
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 30000, // 30秒自动刷新
// 错误重试
errorRetryCount: 3,
errorRetryInterval: 5000,
// 缓存配置
dedupingInterval: 2000,
});
if (error) return <ErrorMessage error={error} onRetry={refreshUser} />;
if (isLoading) return <UserSkeleton />;
return (
<div className="user-profile">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={() => refreshUser()}>
刷新数据
</button>
</div>
);
}
高级功能
javascript
// 条件查询和依赖查询
function Dashboard({ userId }) {
// 用户基础信息
const { data: user } = useSWR(
userId ? `/api/users/${userId}` : null,
fetcher
);
// 依赖查询 - 只有用户信息加载完成后才执行
const { data: posts } = useSWR(
user ? `/api/users/${userId}/posts` : null,
fetcher
);
const { data: stats } = useSWR(
user ? `/api/users/${userId}/stats` : null,
fetcher
);
if (!user) return <DashboardSkeleton />;
return (
<div className="dashboard">
<UserInfo user={user} />
{posts && <PostsList posts={posts} />}
{stats && <StatsWidget stats={stats} />}
</div>
);
}
// 分页数据处理
function PostsList() {
const [page, setPage] = useState(1);
const [allPosts, setAllPosts] = useState([]);
const { data, error, isLoading } = useSWR(
`/api/posts?page=${page}&limit=10`,
fetcher,
{
onSuccess: (newData) => {
if (page === 1) {
setAllPosts(newData.posts);
} else {
setAllPosts(prev => [...prev, ...newData.posts]);
}
}
}
);
const loadMore = () => {
if (data?.hasMore) {
setPage(prev => prev + 1);
}
};
if (error) return <ErrorMessage error={error} />;
return (
<div className="posts-list">
{allPosts.map(post => (
<PostCard key={post.id} post={post} />
))}
{data?.hasMore && (
<button onClick={loadMore} disabled={isLoading}>
{isLoading ? '加载中...' : '加载更多'}
</button>
)}
</div>
);
}
数据变更和乐观更新
"先更新界面,再请求服务器,失败再回滚。"
javascript
// 乐观更新实现
function PostEditor({ postId }) {
const { data: post, mutate } = useSWR(`/api/posts/${postId}`, fetcher);
const updatePost = async (updatedData) => {
try {
// 乐观更新本地数据
const optimisticData = { ...post, ...updatedData };
mutate(optimisticData, false); // false = 不重新验证
// 发送请求到服务器
const response = await fetch(`/api/posts/${postId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedData)
});
if (!response.ok) throw new Error('更新失败');
const serverData = await response.json();
// 用服务器数据更新缓存
mutate(serverData);
// 更新相关的缓存
mutate('/api/posts'); // 刷新文章列表
} catch (error) {
// 错误时恢复原始数据
mutate();
throw error;
}
};
const handleSubmit = async (formData) => {
try {
await updatePost(formData);
toast.success('更新成功');
} catch (error) {
toast.error(error.message);
}
};
if (!post) return <PostSkeleton />;
return (
<PostForm
initialData={post}
onSubmit={handleSubmit}
/>
);
}
SWR 配置和中间件
javascript
import { SWRConfig } from 'swr';
// 全局配置
function App() {
return (
<SWRConfig
value={{
fetcher,
// 全局配置
revalidateOnFocus: false,
revalidateOnReconnect: true,
refreshInterval: 0,
dedupingInterval: 2000,
// 全局错误处理
onError: (error, key) => {
console.error(`SWR Error for ${key}:`, error);
if (error.status === 401) {
window.location.href = '/login';
}
},
// 全局成功处理
onSuccess: (data, key) => {
console.log(`SWR Success for ${key}`);
},
// 错误重试配置
errorRetryCount: 3,
errorRetryInterval: 5000,
// 加载超时
loadingTimeout: 3000,
onLoadingSlow: (key) => {
console.warn(`Slow loading for ${key}`);
},
}}
>
<Router>
<Routes>
{/* 路由配置 */}
</Routes>
</Router>
</SWRConfig>
);
}
SWR 缓存机制
SWR 的缓存机制以"stale-while-revalidate"模式为核心,提供了简洁而高效的数据管理方案。
SWR 缓存策略概览
判断缓存是否新鲜?是否过期?
SWR 的设计理念围绕 数据"新鲜度" (Stale-While-Revalidate 策略),它不是通过传统的"缓存过期时间"控制缓存,而是通过以下几种新鲜度控制机制来实现"数据始终保持接近最新"。
SWR 如何控制缓存"新鲜度"?
控制方式 | 是否过期缓存 | 说明 |
---|---|---|
revalidateOnFocus |
✅ 控制重新获取 | 页面聚焦时是否刷新数据(默认 true ) |
revalidateOnReconnect |
✅ 控制重新获取 | 网络重新连接时是否刷新数据(默认 true ) |
refreshInterval |
✅ 定时刷新 | 设置间隔时间,定时自动重新请求数据 |
dedupingInterval |
⛔ 非缓存过期 | 在该时间内重复请求同一 key,会被忽略(防止频繁请求) |
keepPreviousData |
⛔ 保持旧缓存 | 用于分页/变化型 key 的场景,保持上一次数据 |
cacheProvider |
⛔ 自定义缓存策略 | 若你想实现 TTL(过期控制),可以在自定义 provider 中处理 |
SWR 的缓存不是传统的 TTL 缓存
SWR 的缓存是 "永不过期,但可以随时更新",只要不手动或自动触发更新,它一直存在于缓存中(内存)。
所以:
- 它并不会在 X 秒后自动清除缓存
- 而是提供机制,在适当时机刷新数据
- 页面重新聚焦
- 网络恢复
- 设置
refreshInterval
- 手动
mutate
想实现"缓存过期"怎么办?
需要手动实现类似 TTL(Time-To-Live)的"定时清除缓存"的能力,可以结合 cacheProvider
自定义:
SWR 本身不使用"缓存过期"来控制缓存,而是通过"数据新鲜策略"(聚焦、连接恢复、定时刷新、手动 mutate 等)来保持缓存的数据始终是"接近最新"的。
示例:带过期时间的 localStorage 缓存方案
typescript
ts
复制编辑
const EXPIRATION_MS = 5 * 60 * 1000 // 5分钟
const ttlCacheProvider = () => {
const map = new Map<string, any>()
const raw = localStorage.getItem('swr-ttl-cache')
if (raw) {
const entries: [string, { data: any, timestamp: number }][] = JSON.parse(raw)
for (const [key, value] of entries) {
if (Date.now() - value.timestamp < EXPIRATION_MS) {
map.set(key, value.data)
}
}
}
window.addEventListener('beforeunload', () => {
const entries = Array.from(map.entries()).map(([key, data]) => [key, { data, timestamp: Date.now() }])
localStorage.setItem('swr-ttl-cache', JSON.stringify(entries))
})
return map
}
缓存键管理
javascript
// SWR 缓存键策略
function SWRCacheKeyStrategies() {
// 基础字符串键
const { data: user } = useSWR('/api/user', fetcher);
// 带参数的键
const { data: posts } = useSWR(`/api/posts?page=${page}&limit=${limit}`, fetcher);
// 数组形式的键(推荐)
const { data: userPosts } = useSWR(['posts', 'user', userId], () =>
fetcher(`/api/users/${userId}/posts`)
);
// 条件查询键
const { data: profile } = useSWR(
userId ? ['user', 'profile', userId] : null, // null 会暂停查询
() => fetcher(`/api/users/${userId}/profile`)
);
// 复杂查询键
const { data: searchResults } = useSWR(
['search', { query, filters, sort }],
([, params]) => fetcher('/api/search', { params })
);
return { user, posts, userPosts, profile, searchResults };
}
// 缓存键工厂函数
const cacheKeys = {
users: {
all: () => ['users'],
byId: (id) => ['users', id],
posts: (id) => ['users', id, 'posts'],
profile: (id) => ['users', id, 'profile'],
},
posts: {
all: () => ['posts'],
byCategory: (category) => ['posts', 'category', category],
search: (query) => ['posts', 'search', query],
},
};
缓存配置和重新验证
javascript
// 详细的缓存和重新验证配置
function SWRCacheConfiguration() {
const { data, error, isLoading, mutate } = useSWR(
'/api/user-data',
fetcher,
{
// 重新验证策略
revalidateOnFocus: true, // 窗口获得焦点时重新验证
revalidateOnReconnect: true, // 网络重连时重新验证
revalidateIfStale: true, // 数据过期时重新验证
revalidateOnMount: true, // 组件挂载时重新验证
// 自动刷新
refreshInterval: 30000, // 每30秒自动刷新
refreshWhenHidden: false, // 页面隐藏时不刷新
refreshWhenOffline: false, // 离线时不刷新
// 缓存相关
dedupingInterval: 2000, // 2秒内相同请求去重
focusThrottleInterval: 5000, // 焦点重新验证节流
// 错误处理
errorRetryCount: 3,
errorRetryInterval: 5000,
shouldRetryOnError: (error) => {
// 404错误不重试
return error.status !== 404;
},
// 性能优化
suspense: false, // 是否使用Suspense
fallbackData: undefined, // 初始数据
keepPreviousData: true, // 保持之前的数据直到新数据到达
}
);
return { data, error, isLoading, mutate };
}
手动缓存操作
javascript
import { mutate, cache } from 'swr';
// 缓存操作工具集
function SWRCacheOperations() {
// 获取缓存数据
const getCachedData = (key) => {
return cache.get(key);
};
// 设置缓存数据
const setCacheData = (key, data) => {
mutate(key, data, false); // false = 不重新验证
};
// 更新缓存数据
const updateCacheData = (key, updateFn) => {
mutate(key, updateFn, false);
};
// 删除缓存
const deleteCache = (key) => {
cache.delete(key);
};
// 清空所有缓存
const clearAllCache = () => {
cache.clear();
};
// 批量更新相关缓存
const batchUpdateCache = (updates) => {
updates.forEach(({ key, data, revalidate = false }) => {
mutate(key, data, revalidate);
});
};
// 条件性缓存更新
const conditionalUpdate = (pattern, updateFn) => {
// 遍历所有缓存键
for (const key of cache.keys()) {
if (typeof key === 'string' && key.includes(pattern)) {
const currentData = cache.get(key);
const newData = updateFn(currentData);
if (newData !== currentData) {
mutate(key, newData, false);
}
}
}
};
return {
getCachedData,
setCacheData,
updateCacheData,
deleteCache,
clearAllCache,
batchUpdateCache,
conditionalUpdate,
};
}
乐观更新实现
javascript
// 高级乐观更新模式
function useOptimisticSWR(key, fetcher, config = {}) {
const { data, error, mutate } = useSWR(key, fetcher, config);
// 乐观更新函数
const optimisticUpdate = useCallback(async (
updateData,
requestFn,
rollbackOnError = true
) => {
// 保存当前数据用于回滚
const previousData = data;
try {
// 立即更新UI
mutate(updateData, false);
// 执行实际请求
const result = await requestFn();
// 用服务器数据更新缓存
mutate(result);
return result;
} catch (error) {
// 错误时回滚数据
if (rollbackOnError && previousData !== undefined) {
mutate(previousData, false);
}
throw error;
}
}, [data, mutate]);
return {
data,
error,
mutate,
optimisticUpdate,
};
}
// 乐观更新示例
function TodoItem({ todo }) {
const { data, optimisticUpdate } = useOptimisticSWR(
['todo', todo.id],
() => fetchTodo(todo.id),
{ fallbackData: todo }
);
const toggleComplete = async () => {
const optimisticData = {
...data,
completed: !data.completed,
updatedAt: new Date().toISOString(),
};
try {
await optimisticUpdate(
optimisticData,
() => updateTodo(todo.id, { completed: !data.completed })
);
// 更新相关的todo列表缓存
mutate(['todos'], (todos) => {
return todos.map(t =>
t.id === todo.id ? optimisticData : t
);
}, false);
} catch (error) {
toast.error('更新失败');
}
};
return (
<div className={`todo-item ${data.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={data.completed}
onChange={toggleComplete}
/>
<span>{data.title}</span>
</div>
);
}
缓存预填充和预取
javascript
// SWR 预取策略
function SWRPrefetching() {
// 预取数据
const prefetchData = useCallback((key, fetcher) => {
// 只有缓存中没有数据时才预取
if (!cache.has(key)) {
mutate(key, fetcher(), false);
}
}, []);
// 预填充列表数据中的详情
const prefillDetailsFromList = useCallback((listData) => {
listData.forEach(item => {
const detailKey = ['item', item.id];
if (!cache.has(detailKey)) {
mutate(detailKey, item, false);
}
});
}, []);
// 智能预取下一页
const prefetchNextPage = useCallback((currentPage, hasMore) => {
if (hasMore) {
const nextPageKey = ['posts', 'page', currentPage + 1];
prefetchData(nextPageKey, () => fetchPosts(currentPage + 1));
}
}, [prefetchData]);
return {
prefetchData,
prefillDetailsFromList,
prefetchNextPage,
};
}
// 鼠标悬停预取示例
function PostCard({ post }) {
const { prefetchData } = SWRPrefetching();
const handleMouseEnter = () => {
// 预取文章详情
prefetchData(['post', post.id], () => fetchPostDetail(post.id));
// 预取作者信息
prefetchData(['user', post.authorId], () => fetchUser(post.authorId));
};
return (
<div
className="post-card"
onMouseEnter={handleMouseEnter}
>
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
</div>
);
}
缓存持久化
javascript
// SWR 缓存持久化
function SWRPersistence() {
// 本地存储配置
const localStorageProvider = () => {
// 在初始化时,我们将数据从 localStorage 恢复到 map 中
const map = new Map(JSON.parse(localStorage.getItem('app-cache') || '[]'));
// 在卸载应用程序之前,我们将所有数据写回 localStorage
window.addEventListener('beforeunload', () => {
const appCache = JSON.stringify(Array.from(map.entries()));
localStorage.setItem('app-cache', appCache);
});
return map;
};
// 选择性持久化
const persistentSWR = (key, fetcher, options = {}) => {
return useSWR(key, fetcher, {
...options,
// 只持久化特定数据
use: [
(useSWRNext) => (k, f, config) => {
// 检查是否需要持久化
if (config.persist) {
return useSWRNext(k, f, {
...config,
provider: localStorageProvider,
});
}
return useSWRNext(k, f, config);
}
]
});
};
return { persistentSWR };
}
// 使用持久化缓存
function UserSettings() {
const { data: settings } = useSWR(
'user-settings',
fetchUserSettings,
{
persist: true, // 标记为需要持久化
revalidateOnMount: false,
}
);
return <SettingsPanel settings={settings} />;
}
缓存监控和调试
javascript
// SWR 缓存监控
function SWRCacheMonitor() {
const [cacheEntries, setCacheEntries] = useState([]);
// 监控缓存变化
useEffect(() => {
const updateCacheInfo = () => {
const entries = [];
for (const [key, value] of cache.entries()) {
entries.push({
key: JSON.stringify(key),
hasData: value.data !== undefined,
hasError: value.error !== undefined,
isLoading: value.isLoading,
lastUpdate: value.lastUpdate,
});
}
setCacheEntries(entries);
};
// 定期更新缓存信息
const interval = setInterval(updateCacheInfo, 1000);
updateCacheInfo();
return () => clearInterval(interval);
}, []);
// 缓存操作工具
const clearCache = () => {
cache.clear();
setCacheEntries([]);
};
const invalidateKey = (key) => {
try {
const parsedKey = JSON.parse(key);
mutate(parsedKey);
} catch (error) {
console.error('Invalid key format:', error);
}
};
return (
<div className="cache-monitor">
<div className="monitor-header">
<h3>SWR 缓存监控</h3>
<button onClick={clearCache}>清空缓存</button>
</div>
<div className="cache-stats">
<p>缓存条目数: {cacheEntries.length}</p>
<p>内存使用: {(JSON.stringify([...cache.entries()]).length / 1024).toFixed(2)} KB</p>
</div>
<table className="cache-table">
<thead>
<tr>
<th>缓存键</th>
<th>状态</th>
<th>最后更新</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{cacheEntries.map((entry, index) => (
<tr key={index}>
<td className="key-cell">{entry.key}</td>
<td>
<span className={`status ${entry.hasError ? 'error' : entry.hasData ? 'success' : 'loading'}`}>
{entry.hasError ? '错误' : entry.hasData ? '已缓存' : '加载中'}
</span>
</td>
<td>{entry.lastUpdate ? new Date(entry.lastUpdate).toLocaleTimeString() : '-'}</td>
<td>
<button onClick={() => invalidateKey(entry.key)}>
重新验证
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
// 缓存性能分析
function useCachePerformance() {
const [stats, setStats] = useState({
hits: 0,
misses: 0,
totalRequests: 0,
});
useEffect(() => {
let hits = 0;
let misses = 0;
// 监听缓存事件(需要自定义实现)
const originalFetcher = window.fetch;
window.fetch = async (...args) => {
const cacheKey = args[0];
const hasCachedData = cache.has(cacheKey);
if (hasCachedData) {
hits++;
} else {
misses++;
}
setStats(prev => ({
hits: prev.hits + (hasCachedData ? 1 : 0),
misses: prev.misses + (hasCachedData ? 0 : 1),
totalRequests: prev.totalRequests + 1,
}));
return originalFetcher(...args);
};
return () => {
window.fetch = originalFetcher;
};
}, []);
const hitRate = stats.totalRequests > 0 ? (stats.hits / stats.totalRequests * 100).toFixed(2) : 0;
return {
...stats,
hitRate: `${hitRate}%`,
};
}
性能对比分析
Bundle 大小对比
javascript
// 包大小分析(gzip压缩后)
const bundleAnalysis = {
'TanStack Query': {
core: '~39KB',
devtools: '~45KB',
total: '~84KB'
},
'SWR': {
core: '~24KB',
devtools: 'N/A',
total: '~24KB'
}
};
// 性能基准测试
class PerformanceBenchmark {
static async measureCacheHit(library, iterations = 1000) {
const startTime = performance.now();
for (let i = 0; i < iterations; i++) {
// 模拟缓存命中
await library.getFromCache(`test-key-${i % 100}`);
}
const endTime = performance.now();
return endTime - startTime;
}
static async runComparison() {
const results = {
tanstackQuery: {
cacheHit: await this.measureCacheHit(tanstackQuery),
revalidation: await this.measureRevalidation(tanstackQuery)
},
swr: {
cacheHit: await this.measureCacheHit(swr),
revalidation: await this.measureRevalidation(swr)
}
};
console.table(results);
return results;
}
}
内存缓存机制对比
是的,TanStack Query 和 SWR 默认都使用的是内存缓存(Memory Cache),但它们的行为、设计哲学和扩展能力有明显区别。
特性 | SWR | TanStack Query (React Query) |
---|---|---|
✅ 是否默认内存缓存 | ✅ 是,基于 Map() |
✅ 是,使用 QueryCache (内部也是内存) |
🧠 缓存结构 | Map<key, data> 简单结构 |
结构复杂,包含 metadata、状态、observer 等 |
🧮 缓存元信息 | ❌ 无元信息,只是简单的缓存值 | ✅ 有完整 metadata(状态、时间戳、是否 stale、是否 active) |
📦 缓存作用域 | 全局共享(同一 App 内) | 全局共享(但可通过 QueryClient 自定义作用域) |
🔄 自动清除机制 | ❌ 默认不会清除 | ✅ 有 cacheTime ,超过自动 GC |
⏱ 是否支持过期控制 | ❌ 不支持,需手动实现 TTL | ✅ staleTime / cacheTime 内建 |
🔧 是否可自定义缓存 | ✅ 支持 cacheProvider 替换(如 localStorage) |
✅ 支持自定义 QueryCache ,但需要更复杂的扩展 |
两者默认缓存底层是怎么实现的?
🚀 SWR
默认缓存结构:
ts
const cache = new Map<string, any>()
你也可以通过 SWRConfig
替换:
tsx
<SWRConfig value={{ provider: () => new Map() }}>
<App />
</SWRConfig>
🚀 TanStack Query
默认缓存是通过 QueryClient
持有的 QueryCache
管理:
ts
const queryClient = new QueryClient({
defaultOptions: {
queries: {
cacheTime: 5 * 60 * 1000, // 默认 5 分钟自动清理
staleTime: 0 // 默认立即 stale
}
}
})
它的缓存结构是:
ts
Map<queryHash, QueryInstance>
每个 QueryInstance
包含:
- data
- dataUpdatedAt
- isStale
- observers(组件)
- status / fetchStatus
- retryCount 等 metadata
两者默认都是基于"内存缓存",但 TanStack Query 更复杂更智能,而 SWR 更轻量更灵活。
对比项 | SWR | TanStack Query |
---|---|---|
内存缓存结构 | 简单 key-value | 结构化、有状态 |
生命周期控制 | 手动 | 自动(staleTime / cacheTime) |
扩展性 | 非常灵活 | 高度可配置但复杂 |
使用场景 | 轻量级请求、组件驱动 | 复杂应用、分页、并发请求管理 |
如你要做缓存持久化、缓存同步跨 Tab、缓存加密等扩展,两者都可以做到,但实现路径略有不同
使用场景选择
选择决策树
具体使用场景
javascript
// 场景1: 简单的数据展示应用 - 推荐 SWR
function SimpleApp() {
const ProfilePage = () => {
const { data: user } = useSWR('/api/user', fetcher);
const { data: posts } = useSWR('/api/posts', fetcher);
return (
<div>
<UserCard user={user} />
<PostsList posts={posts} />
</div>
);
};
return <ProfilePage />;
}
// 场景2: 复杂的企业级应用 - 推荐 TanStack Query
function EnterpriseApp() {
const DashboardPage = () => {
// 复杂的并行查询
const userQuery = useQuery({
queryKey: ['user'],
queryFn: fetchUser
});
const projectsQuery = useQuery({
queryKey: ['projects', userQuery.data?.id],
queryFn: () => fetchProjects(userQuery.data.id),
enabled: !!userQuery.data?.id
});
// 无限滚动
const tasksQuery = useInfiniteQuery({
queryKey: ['tasks'],
queryFn: fetchTasks,
getNextPageParam: (lastPage) => lastPage.nextCursor
});
return (
<div className="dashboard">
<UserSection user={userQuery.data} />
<ProjectsGrid projects={projectsQuery.data} />
<TasksList tasks={tasksQuery.data?.pages} />
</div>
);
};
return <DashboardPage />;
}
数据状态细化对比
特性 | SWR | TanStack Query |
---|---|---|
缓存是否"stale" | 自动 + 模糊控制 | 明确用 staleTime 控制 |
缓存是否"过期" | 默认不过期(除非页面刷新) | 有 cacheTime 过期回收 |
缓存是否清除 | 页面刷新或手动触发 | cacheTime 自动清除 |
缓存元信息 | 无(以 value 为主) | 有详细状态、时间戳等元信息 |
最佳实践
TanStack Query 最佳实践
javascript
// 1. 查询键管理
const queryKeys = {
users: {
all: ['users'] as const,
lists: () => [...queryKeys.users.all, 'list'] as const,
list: (filters: UserFilters) =>
[...queryKeys.users.lists(), { filters }] as const,
details: () => [...queryKeys.users.all, 'detail'] as const,
detail: (id: string) => [...queryKeys.users.details(), id] as const,
},
};
// 2. 自定义Hook封装
function useUser(userId: string) {
return useQuery({
queryKey: queryKeys.users.detail(userId),
queryFn: () => api.get(`/users/${userId}`).then(res => res.data),
staleTime: 5 * 60 * 1000,
select: (data) => ({
...data,
fullName: `${data.firstName} ${data.lastName}`
})
});
}
SWR 最佳实践
javascript
// 1. 自定义Hook模式
function useUser(userId?: string) {
const { data, error, isLoading, mutate } = useSWR(
userId ? `/api/users/${userId}` : null,
{
revalidateOnMount: true,
dedupingInterval: 5000
}
);
return {
user: data,
isLoading,
isError: !!error,
error,
refresh: mutate
};
}
// 2. 乐观更新工具
function useOptimisticUpdate() {
return useCallback(async (
key: string,
updateFn: (data: any) => any,
mutationFn: () => Promise<any>
) => {
try {
// 获取当前数据
const currentData = cache.get(key);
// 乐观更新
const optimisticData = updateFn(currentData);
mutate(key, optimisticData, false);
// 执行实际请求
const result = await mutationFn();
// 更新为服务器数据
mutate(key, result);
return result;
} catch (error) {
// 回滚数据
mutate(key);
throw error;
}
}, []);
}
迁移指南
从 SWR 迁移到 TanStack Query
javascript
// SWR 代码
function UserProfile({ userId }) {
const { data: user, error, mutate } = useSWR(
`/api/users/${userId}`,
fetcher,
{ revalidateOnFocus: false }
);
if (error) return <div>Error loading user</div>;
if (!user) return <div>Loading...</div>;
return <UserCard user={user} />;
}
// 迁移到 TanStack Query
function UserProfile({ userId }) {
const { data: user, error, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => api.get(`/users/${userId}`).then(res => res.data),
refetchOnWindowFocus: false
});
if (error) return <div>Error loading user</div>;
if (isLoading) return <div>Loading...</div>;
return <UserCard user={user} />;
}
总结
TanStack Query 和 SWR 都是优秀的数据获取库,选择哪个主要取决于项目需求和团队偏好:
选择 TanStack Query 当:
- 需要复杂的查询功能(无限滚动、并行查询、依赖查询)
- 需要强大的开发工具和调试能力
- 项目规模较大,需要企业级特性
- 团队有足够的学习时间和经验
选择 SWR 当:
- 项目相对简单,主要是基础的CRUD操作
- 希望保持较小的包大小
- 团队更偏向于简单直接的API
- 快速原型开发或小型项目
无论选择哪个库,都应该:
- 建立良好的错误处理机制
- 合理配置缓存策略
- 实现适当的加载状态处理
- 考虑性能优化和监控
- 编写全面的测试覆盖