SWR vs React Query:全面比较 React 数据获取解决方案
SWR
SWR 是由 Vercel 团队开发的轻量级数据获取 React Hooks 库。名称 "SWR" 来源于 HTTP 缓存策略 "stale-while-revalidate",这一策略允许应用立即从缓存(可能已过时)返回数据,同时在后台发送请求验证并最终使用最新数据更新页面。
React Query (TanStack Query)
React Query 是一个由 TanStack 维护的功能全面的异步状态管理库,现已更名为 TanStack Query。它不仅解决了数据获取问题,还提供了复杂的缓存管理、服务器状态同步以及数据更新等功能。
核心概念对比
特性 | SWR | React Query |
---|---|---|
核心理念 | 先返回缓存,后台重新验证 | 服务器状态与客户端状态分离管理 |
设计焦点 | 简单、轻量、高性能 | 全面的异步状态管理解决方案 |
体积 | 约 7kb (gzipped) | 约 12kb (gzipped) |
首次出现 | 2019年底 | 2019年中 |
社区支持 | Vercel团队主导 | TanStack社区支持 |
主要目标 | 提供简单直观的数据获取体验 | 提供完整的服务器状态管理解决方案 |
基本用法对比
SWR 基本用法
ts
import useSWR from 'swr';
const fetcher = url => fetch(url).then(res => res.json());
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher);
if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;
return <div>Hello, {data.name}!</div>;
}
React Query 基本用法
ts
import { useQuery } from '@tanstack/react-query';
const fetchUser = () => fetch('/api/user').then(res => res.json());
function Profile() {
const { data, error, isLoading } = useQuery({
queryKey: ['user'],
queryFn: fetchUser
});
if (error) return <div>Failed to load</div>;
if (isLoading) return <div>Loading...</div>;
return <div>Hello, {data.name}!</div>;
}
关键特性对比
缓存管理
SWR 缓存管理
SWR 使用基于键的简单缓存系统,每个请求都以其键为标识符存储在全局缓存中。
ts
// 使用字符串键
const { data } = useSWR('/api/user', fetcher);
// 使用数组键(包含参数)
const { data } = useSWR(['/api/user', token], ([url, token]) => fetchWithToken(url, token));
SWR 的缓存持久策略相对简单,没有提供复杂的缓存配置选项。缓存在不使用时可能需要手动清理以避免内存泄漏。
ts
const { cache } = useSWRConfig();
// 手动清除缓存
cache.delete('/api/user');
React Query 缓存管理
React Query 提供了更复杂的缓存系统,支持查询键、缓存时间控制和自动垃圾回收:
ts
// 使用查询键
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
// 配置缓存时间
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5分钟内数据保持新鲜
cacheTime: 10 * 60 * 1000 // 10分钟后从缓存中移除
});
React Query 自动管理缓存生命周期,可以精确控制数据何时过期、何时被移除。
自动重新验证
SWR 自动重新验证
SWR 提供了多种触发重新验证的方式:
ts
const { data } = useSWR('/api/user', fetcher, {
// 窗口聚焦时重新验证
revalidateOnFocus: true,
// 网络恢复时重新验证
revalidateOnReconnect: true,
// 定时轮询(每10秒)
refreshInterval: 10000,
// 切换到其他标签页再回来时,限制5分钟内只刷新一次
focusThrottleInterval: 5 * 60 * 1000
});
React Query 自动重新验证
React Query 也提供了类似的自动重新获取机制:
ts
const { data } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
// 窗口聚焦时重新获取
refetchOnWindowFocus: true,
// 网络恢复时重新获取
refetchOnReconnect: true,
// 定时轮询
refetchInterval: 10000,
// 仅当标签页处于活动状态时才轮询
refetchIntervalInBackground: false
});
React Query 的自动重新获取配置更加灵活,可以通过 QueryClient
全局配置或针对特定查询定制。
数据更新与突变
SWR 数据更新
SWR 提供了两种主要的数据更新方式:mutate
和 useSWRMutation
:
ts
// 全局 mutate
import { mutate } from 'swr';
mutate('/api/user', newData);
// 绑定数据的 mutate
const { data, mutate } = useSWR('/api/user', fetcher);
// 本地更新
mutate(newData, false); // 第二个参数为 false 表示不重新验证
// useSWRMutation 用于手动触发的操作
const { trigger } = useSWRMutation('/api/user', updateUser);
SWR 还支持乐观更新:
ts
mutate('/api/todos', async todos => {
const newTodo = { id: Date.now(), text: 'New Todo' };
// 1. 更新本地数据立即反映在UI上
const updatedTodos = [...todos, newTodo];
// 2. 发送请求
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo)
});
// 3. 返回更新后的数据
return updatedTodos;
}, { optimisticData: [...todos, newTodo], rollbackOnError: true });
React Query 数据更新
React Query 使用 useMutation
来处理数据更新操作:
ts
const { mutate } = useMutation({
mutationFn: (newUser) => updateUser(newUser),
// 乐观更新
onMutate: async (newUser) => {
// 取消相关查询以避免冲突
await queryClient.cancelQueries({ queryKey: ['user'] });
// 保存之前的值
const previousUser = queryClient.getQueryData(['user']);
// 乐观更新缓存
queryClient.setQueryData(['user'], newUser);
// 返回回滚上下文
return { previousUser };
},
// 如果失败,回滚到上一个状态
onError: (err, newUser, context) => {
queryClient.setQueryData(['user'], context.previousUser);
},
// 成功后使相关查询无效,触发重新获取
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user'] });
}
});
React Query 的突变 API 更加全面,提供了更多的生命周期回调和集成点。
API设计
SWR API设计
SWR 的 API 设计简洁直观,主要围绕 useSWR
和 useSWRMutation
两个 hooks:
ts
// 基本用法
const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher, options);
// 不可变数据
const { data } = useSWRImmutable(key, fetcher);
// 手动触发的数据更新
const { trigger, isMutating } = useSWRMutation(key, fetcher);
SWR 的 API 设计遵循简单性原则,大多数情况下只需要传递一个键和一个获取函数。
React Query API设计
React Query 提供了一套更全面的 API,包括查询和突变操作:
ts
// 基本查询
const { data, error, isLoading, isFetching, ... } = useQuery(options);
// 无限查询(分页、加载更多)
const { data, fetchNextPage, hasNextPage, ... } = useInfiniteQuery(options);
// 突变操作
const { mutate, isLoading, isError, ... } = useMutation(options);
// 查询失效
queryClient.invalidateQueries({ queryKey: ['todos'] });
React Query 的 API 更加明确地区分了查询和突变操作,并提供了额外的工具来管理复杂的数据获取场景。
错误处理与重试机制
SWR 错误处理
SWR 提供了简单的错误处理和重试机制:
ts
const { data, error } = useSWR('/api/user', fetcher, {
onError: (err) => {
console.error('请求失败:', err);
},
errorRetryCount: 3, // 失败后重试3次
errorRetryInterval: 5000 // 每次重试间隔5秒
});
React Query 错误处理
React Query 提供了更强大的错误处理和重试功能:
ts
const { data, error } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
onError: (err) => {
console.error('请求失败:', err);
},
retry: 3, // 重试次数
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // 指数退避
useErrorBoundary: true // 错误可以被 React Error Boundary 捕获
});
React Query 还支持根据错误类型决定是否重试:
ts
retry: (failureCount, error) => {
if (error.status === 404) return false; // 不重试404错误
return failureCount < 3; // 其他错误重试3次
}
性能优化
SWR 性能优化
SWR 通过几种方式来优化性能:
ts
// 请求去重
// 相同时间内对同一键的多个请求会被合并
const { data: user1 } = useSWR('/api/user', fetcher);
const { data: user2 } = useSWR('/api/user', fetcher); // 不会发起新请求
// 使用 suspense 模式
const { data } = useSWR('/api/user', fetcher, { suspense: true });
// 预加载数据
import { preload } from 'swr';
preload('/api/user', fetcher);
React Query 性能优化
React Query 提供了更多性能优化选项:
ts
// 查询函数只在特定条件下执行
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId // 只有当 userId 存在时才执行查询
});
// 数据变换
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (users) => users.filter(user => user.active) // 转换返回数据
});
// 预取数据
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
// 延迟数据请求
const { refetch } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
enabled: false // 初始不执行查询
});
React Query 还可以通过 setQueryDefaults
设置查询默认值,避免在多个组件中重复配置。
生态系统集成
SWR 生态系统
SWR 生态相对简单,主要由核心库和一些插件组成:
- 与 Next.js 深度集成
swr/infinite
用于分页请求swr/mutation
用于数据更新
SWR 被设计为轻量级解决方案,没有太多外部依赖。
React Query 生态系统
React Query 有更广泛的生态系统:
@tanstack/react-query-devtools
用于开发调试- 支持多种框架:Vue Query、Svelte Query 等
- 插件系统支持扩展功能
- 与其他 TanStack 库(如 TanStack Router)集成
React Query 还提供了一个强大的开发者工具,可以实时查看查询状态、缓存内容和请求详情。
使用场景分析
适合 SWR 的场景
- 需要轻量级解决方案的项目
- 与 Next.js 集成的应用
- 简单的数据获取需求
- 首屏加载性能至关重要的应用
- 对包大小敏感的项目
适合 React Query 的场景
- 复杂的数据管理需求
- 需要处理分页、无限滚动等高级数据获取模式
- 需要精细控制缓存策略的项目
- 有大量数据更新和突变操作的应用
- 需要强大开发者工具支持的团队
- 使用多种状态管理模式的大型应用
源码实现比较
SWR 源码更为精简,主要围绕核心的数据获取和缓存逻辑。React Query 源码更为复杂,采用了更模块化的设计,支持更多高级功能。
两者在内部实现上有一些关键差异:
- 缓存实现: SWR 使用简单的 Map 结构,React Query 使用更复杂的缓存系统,支持垃圾回收
- 订阅机制: 两者都使用发布-订阅模式,但 React Query 实现了更精细的事件系统
- 并发请求处理: React Query 有更复杂的去重和请求优先级处理
- 类型系统: React Query 在 TypeScript 类型定义上更加完善
优缺点总结
SWR 优点
- 简单轻量: API 简洁直观,学习曲线平缓
- 性能优先: 包体积小,运行效率高
- 与 Next.js 深度集成: 在 Next.js 项目中使用体验极佳
- 自动重新验证: 内置多种数据更新策略
- 即时反馈: 乐观更新支持良好
SWR 缺点
- 功能相对有限: 缺少一些复杂数据管理功能
- 缓存管理简单: 没有提供复杂的缓存控制选项
- 开发工具缺乏: 没有内置的开发者调试工具
- 全局 key 命名问题: 可能导致命名冲突
- 没有请求中断 API: 无法优雅地取消请求
- 缺少 getter 方法: 无法在组件外部直接访问缓存数据
React Query 优点
- 功能全面: 支持几乎所有数据获取场景
- 缓存控制精细: 提供丰富的缓存配置选项
- 开发工具强大: 内置调试工具帮助开发和排查问题
- 查询无效化机制: 可以精确控制数据何时需要重新获取
- 强大的突变 API: 完整的生命周期钩子支持各种数据更新场景
- 类型支持优秀: TypeScript 定义全面
React Query 缺点
- 学习曲线较陡: 概念和 API 较多,需要时间掌握
- 包体积较大: 相比 SWR 体积更大
- 配置选项过多: 有时需要更多的初始设置
- 可能过度设计: 简单场景使用可能显得繁琐
- 不适合小型项目: 对于简单需求可能是杀鸡用牛刀
实践案例
使用 SWR 实现待办事项应用
ts
import useSWR, { useSWRConfig } from 'swr';
import { useState } from 'react';
// 定义获取函数
const fetcher = url => fetch(url).then(res => res.json());
// 创建 Todo
async function addTodo(url, { arg }) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(arg)
});
if (!res.ok) throw new Error('添加失败');
return res.json();
}
function TodoList() {
const [newTodo, setNewTodo] = useState('');
const { mutate } = useSWRConfig();
// 获取待办事项列表
const { data: todos = [], error, isLoading } = useSWR('/api/todos', fetcher);
const handleAddTodo = async (e) => {
e.preventDefault();
const newTodoItem = { id: Date.now(), title: newTodo, completed: false };
// 乐观更新
mutate('/api/todos', async (currentTodos) => {
try {
// 先更新本地数据
const updatedTodos = [...currentTodos, newTodoItem];
// 发送请求
await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodoItem)
});
setNewTodo('');
return updatedTodos;
} catch (error) {
// 发生错误时不更新
return currentTodos;
}
}, { optimisticData: [...todos, newTodoItem], revalidate: false });
};
if (error) return <div>Failed to load todos</div>;
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>Todo List</h1>
<form onSubmit={handleAddTodo}>
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="添加新待办..."
/>
<button type="submit">添加</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
}
使用 React Query 实现相同功能
ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
// 定义获取函数
const fetchTodos = async () => {
const res = await fetch('/api/todos');
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
};
// 创建 Todo 函数
const addTodo = async (newTodo) => {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo)
});
if (!res.ok) throw new Error('添加失败');
return res.json();
};
function TodoList() {
const [newTodo, setNewTodo] = useState('');
const queryClient = useQueryClient();
// 获取待办事项
const { data: todos = [], isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos
});
// 添加待办事项的突变
const addMutation = useMutation({
mutationFn: addTodo,
onMutate: async (newTodoItem) => {
// 取消正在进行的请求
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 保存当前数据
const previousTodos = queryClient.getQueryData(['todos']);
// 乐观更新UI
queryClient.setQueryData(['todos'], old => [...old, newTodoItem]);
// 返回之前的数据用于回滚
return { previousTodos };
},
onError: (err, newTodo, context) => {
// 发生错误时回滚到之前的数据
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
// 请求完成后刷新数据
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
const handleAddTodo = (e) => {
e.preventDefault();
const newTodoItem = { id: Date.now(), title: newTodo, completed: false };
addMutation.mutate(newTodoItem);
setNewTodo('');
};
if (error) return <div>Failed to load todos</div>;
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>Todo List</h1>
<form onSubmit={handleAddTodo}>
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="添加新待办..."
/>
<button type="submit" disabled={addMutation.isPending}>
{addMutation.isPending ? '添加中...' : '添加'}
</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
}
如何选择合适的库
-
项目规模与复杂度:
- 小型项目或简单数据需求: SWR
- 大型项目或复杂数据管理需求: React Query
-
团队熟悉度:
- 希望简单易上手: SWR
- 愿意投入时间学习更强大的工具: React Query
-
特定功能需求:
- 需要强大的开发者工具: React Query
- 需要精细的缓存控制: React Query
- 追求轻量级实现: SWR
- 与 Next.js 深度集成: SWR
-
性能考量:
- 注重首屏加载速度和包体积: SWR
- 需要处理大量数据和复杂的查询关系: React Query
无论选择哪个库,都建议遵循以下最佳实践:
- 封装数据获取逻辑: 创建自定义 hooks 封装特定资源的获取
- 统一错误处理: 设置全局错误处理策略
- 合理设置缓存策略: 根据数据特性设置适当的缓存和重新验证策略
- 利用预加载: 提前加载可能需要的数据
- 合理使用乐观更新: 提高用户体验的同时确保数据一致性
- 避免过度重新获取: 调整重新验证的条件和频率