在写前端应用的时候,我发现自己经常重复着同样的模式:useEffect + fetch + useState。刚开始觉得这很自然,不就是请求数据、设置状态嘛。但随着项目变大,我越来越感觉不对劲:为什么到处都是重复的 loading、error 处理?为什么同一个用户信息要请求好几次?为什么数据更新后其他组件不同步?
直到接触了 React Query,我才恍然大悟:原来服务端数据和普通状态根本不是一回事。这篇文章是我重新理解这个问题的过程,也希望能帮到同样困惑的你。
一个完整的对比:用户资料页
让我们从一个实际场景开始。假设要做一个用户资料页,需要:
- 显示用户信息(名字、头像等)
- 支持编辑昵称
- 处理加载状态和错误
- 多个组件共享用户数据(Header 和 Profile 都需要)
听起来不复杂对吧?我们先看看用传统方式怎么写。
方案 1:用 useState(传统方式)
// 环境:React
// 场景:用传统方式管理服务端数据
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// 获取数据
useEffect(() => {
setLoading(true);
setError(null);
fetch(`/api/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
})
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [userId]);
// 更新昵称
const updateName = async (newName) => {
try {
const res = await fetch(`/api/users/${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName }),
});
if (!res.ok) throw new Error('Update failed');
const updatedUser = await res.json();
setUser(updatedUser); // 更新本地状态
} catch (err) {
alert('fail to update');
}
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return null;
return (
<div>
<h1>{user.name}</h1>
<button onClick={() => updateName('new name')}>
Update Name
</button>
</div>
);
}
看起来没问题?但等等,我们还需要在 Header 组件里显示用户名:
// Header 也需要用户信息,又要写一遍
function Header({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// 又是一模一样的逻辑...
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
return <div>{user?.name}</div>;
}
这时候问题就来了:
❌ 问题 1:代码重复 Profile 和 Header 各自管理状态,同样的请求逻辑写了两遍。更糟的是,如果它们同时渲染,会发起两次相同的网络请求!
❌ 问题 2:缓存缺失 用户从 Profile 页跳到其他页,再跳回来,组件重新渲染 → useEffect 触发 → 又请求一遍。即使数据根本没变,用户也要看着 loading 转圈。
❌ 问题 3:数据同步困难 在 Profile 页改了昵称,更新了本地的 user 状态。但 Header 组件里的昵称怎么更新?它们是两个独立的 useState,互不影响!
❌ 问题 4:缺少错误重试 网络请求失败了,用户只能刷新页面重试。我们得手动实现一个"重试"按钮,还要管理重试次数、延迟等逻辑。
❌ 问题 5:缺少乐观更新 点击更新按钮 → 等待请求完成 → 界面才变化。用户会觉得卡顿,体验很差。
方案 2:用 React Query
现在让我们看看 React Query 如何解决这些问题:
// 环境:React + React Query
// 场景:用专业工具管理服务端数据
// 依赖:npm install @tanstack/react-query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// 封装获取用户的函数
const fetchUser = async (userId) => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
};
// 封装更新用户的函数
const updateUser = async ({ userId, data }) => {
const res = await fetch(`/api/users/${userId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error('Update failed');
return res.json();
};
function UserProfile({ userId }) {
const queryClient = useQueryClient();
// 获取数据(自动处理 loading、error、缓存)
const { data: user, isLoading, error, refetch } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5分钟内认为数据是新鲜的
retry: 3, // 失败自动重试 3 次
});
// 更新数据(支持乐观更新、自动同步)
const updateMutation = useMutation({
mutationFn: updateUser,
// 乐观更新:立即显示效果
onMutate: async (newData) => {
await queryClient.cancelQueries({ queryKey: ['user', userId] });
const previousUser = queryClient.getQueryData(['user', userId]);
queryClient.setQueryData(['user', userId], (old) => ({
...old,
...newData.data,
}));
return { previousUser };
},
// 失败时回滚
onError: (err, newData, context) => {
queryClient.setQueryData(['user', userId], context.previousUser);
alert('fail to update');
},
// 成功后让缓存失效(确保数据最新)
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user', userId] });
},
});
const handleUpdateName = () => {
updateMutation.mutate({
userId,
data: { name: 'new name' },
});
};
if (isLoading) return <div>Loading...</div>;
if (error) return (
<div>
Error: {error.message}
<button onClick={() => refetch()}>retry</button>
</div>
);
return (
<div>
<h1>{user.name}</h1>
<button
onClick={handleUpdateName}
disabled={updateMutation.isPending}
>
{updateMutation.isPending ? 'Updating...' : 'Update Name'}
</button>
</div>
);
}
// Header 组件(复用相同的数据)
function Header({ userId }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
return <div>Welcome, {user?.name}</div>;
}
// Profile 和 Header 同时渲染:
// ✅ 只发一次请求(自动去重)
// ✅ Profile 更新后,Header 自动同步
// ✅ 数据缓存,不会重复请求
对比总结
| 功能 | useState 方案 | React Query 方案 |
|---|---|---|
| 代码量 | ~40 行 | ~30 行 |
| loading/error | 手动管理 | 自动处理 ✅ |
| 缓存 | 无 | 自动缓存 ✅ |
| 去重请求 | 否(发多次) | 自动去重 ✅ |
| 数据同步 | 手动(很难) | 自动同步 ✅ |
| 错误重试 | 无 | 自动重试 ✅ |
| 乐观更新 | 需要手写 | 内置支持 ✅ |
| 缓存失效 | 无概念 | 自动管理 ✅ |
核心区别是什么呢?我的理解是:
- useState:把服务端数据当"普通状态",所有问题都要自己处理
- React Query:把服务端数据当"远程缓存",这些问题工具已经解决了
这让我开始思考:服务端数据和客户端状态到底有什么本质区别?
理解核心概念:什么是服务端状态?
客户端状态 vs 服务端状态
我觉得理解这个区别很重要。先看客户端状态:
// 客户端状态:数据的"真相源"在客户端
// Modal 开关
const [isOpen, setIsOpen] = useState(false);
setIsOpen(true); // 立即生效,没有"失败"的可能
// 主题设置
const [theme, setTheme] = useState('dark');
setTheme('light'); // 改了就是改了
// 表单输入
const [name, setName] = useState('');
setName('new name'); // 完全在客户端控制
这些状态的特点是:你说是什么就是什么。没有网络请求,没有失败的可能,刷新页面就丢失。
但服务端状态完全不同:
// 服务端状态:数据的"真相源"在服务器
// 用户信息
const [user, setUser] = useState(null);
fetch('/api/user').then(setUser);
// 问题:
// - 可能请求失败(需要重试)
// - 数据可能过期(服务器上改了)
// - 多处使用时(需要缓存)
// - 刷新页面就丢了(需要重新获取)
关键区别在哪?我整理了这个对比:
| 维度 | 客户端状态 | 服务端状态 |
|---|---|---|
| 真相源 | 客户端 | 服务器 |
| 持久性 | 刷新即丢失 | 服务器持久化 |
| 同步问题 | 无(单一真相) | 有(客户端是副本) |
| 失效判断 | 不需要 | 需要(数据会过期) |
| 网络请求 | 无 | 有(可能失败) |
| 缓存需求 | 无 | 有(避免重复请求) |
| 适用工具 | useState/Zustand | React Query/SWR |
一个生动的比喻
我想到了一个比喻帮助理解:
客户端状态 = 你的笔记本
- 你写什么就是什么
- 想改就改,没有"失败"
- 丢了就丢了(刷新页面)
服务端状态 = 图书馆的书
- 你只是借阅,真正的书在图书馆
- 借之前要去图书馆拿(fetch)
- 可能借不到(404)
- 别人可能改了内容(需要同步)
- 需要判断是否过期(stale)
- 同一本书可以借给多人(缓存共享)
用 useState 管理"图书馆的书",就像你自己抄了一份。但书的内容变了,你的副本怎么办?这就是为什么需要 React Query。
服务端状态的特殊生命周期
服务端状态有个复杂的生命周期,我试着用流程图表示:
请求数据
↓
Loading (加载中)
↓
成功? ←────────┐
↓ │
是 否 │
↓ ↓ │
Fresh Error │
(新鲜) (失败) │
↓ ↓ │
使用 重试? ────┘
数据 ↓
↓ 否
过期? ↓
↓ 显示错误
是
↓
Stale (过期)
↓
后台重新获取
↓
Fresh (新鲜)
这个流程里有很多 useState 无法处理的问题:
- ❌ 失败重试(Error → 重试)
- ❌ 数据过期判断(Fresh → Stale)
- ❌ 后台自动刷新(Stale → 重新获取)
- ❌ 缓存共享(多个组件用同一份数据)
React Query 核心原理
理解了问题,我们再看 React Query 是如何解决的。
Query Key:缓存的唯一标识
React Query 用 Query Key 来标识不同的缓存:
// 环境:React + React Query
// 场景:Query Key 的使用
// 不同的 key = 不同的缓存
useQuery({
queryKey: ['user', 123], // 用户 123 的数据
queryFn: () => fetchUser(123),
});
useQuery({
queryKey: ['user', 456], // 用户 456 的数据
queryFn: () => fetchUser(456),
});
// 相同的 key = 共享缓存
// 组件 A
useQuery({
queryKey: ['user', 123],
queryFn: () => fetchUser(123),
});
// 组件 B(使用相同的缓存,不会重复请求)
useQuery({
queryKey: ['user', 123],
queryFn: () => fetchUser(123),
});
Query Key 的设计很重要。我总结了一些原则:
// ❌ 不好:key 太简单
useQuery({
queryKey: ['user'], // 所有用户共享一个缓存?
queryFn: () => fetchUser(userId),
});
// ✅ 好:key 包含所有影响数据的参数
useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// ✅ 更好:包含查询条件
useQuery({
queryKey: ['users', { status: 'active', page: 1 }],
queryFn: () => fetchUsers({ status: 'active', page: 1 }),
});
// 规则:key 变化 = 重新请求
缓存的生命周期
React Query 的缓存有几个状态:
// 环境:React + React Query
// 场景:缓存生命周期配置
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
// 缓存配置
staleTime: 5 * 60 * 1000, // 5分钟内认为数据"新鲜"
gcTime: 10 * 60 * 1000, // 缓存保留 10 分钟
});
// 数据状态流转:
// 1. fresh(新鲜):刚获取,不会重新请求
// 2. stale(过期):超过 staleTime,可能会重新请求
// 3. inactive(无人使用):所有组件都卸载了
// 4. deleted(删除):超过 gcTime,从内存中删除
我举个实际例子帮助理解:
// t=0s: 用户打开 Profile 页
useQuery(['user', 123], fetchUser);
// → 发起请求
// → 数据返回,状态:fresh
// t=10s: 用户跳到其他页,又跳回 Profile
useQuery(['user', 123], fetchUser);
// → 缓存是 fresh(未超过 5 分钟)
// → 直接用缓存,不发请求 ✅
// t=6min: 用户再次打开 Profile
useQuery(['user', 123], fetchUser);
// → 缓存是 stale(超过 5 分钟)
// → 先显示缓存数据(不用等待)
// → 后台重新请求更新 ✅
// t=15min: 缓存已删除
useQuery(['user', 123], fetchUser);
// → 重新请求
这种机制让用户体验特别好:大部分时候直接显示缓存,不用等待,同时又能保证数据最新。
自动的后台刷新
React Query 还支持自动刷新策略:
// 环境:React + React Query
// 场景:配置自动刷新
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
// 自动刷新策略
refetchOnWindowFocus: true, // 窗口获得焦点时刷新
refetchOnReconnect: true, // 网络恢复时刷新
refetchInterval: 30000, // 每 30 秒刷新一次
});
// 用户体验:
// 1. 用户切换到别的标签页
// 2. 5分钟后切回来
// 3. React Query 自动刷新数据
// 4. 用户看到的永远是最新数据 ✅
Mutation:修改数据
查询数据之外,我们还需要修改数据。React Query 用 useMutation 处理:
// 环境:React + React Query
// 场景:修改数据并同步缓存
import { useMutation, useQueryClient } from '@tanstack/react-query';
function EditProfile({ userId }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newData) => updateUser(userId, newData),
// 成功后的操作
onSuccess: (updatedUser) => {
// 方案 1:直接更新缓存(不需要重新请求)
queryClient.setQueryData(['user', userId], updatedUser);
// 方案 2:让缓存失效,触发重新获取
queryClient.invalidateQueries({ queryKey: ['user', userId] });
// 方案 3:如果有列表页,也让列表失效
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
return (
<button
onClick={() => mutation.mutate({ name: '新名字' })}
disabled={mutation.isPending}
>
{mutation.isPending ? '保存中...' : '保存'}
</button>
);
}
乐观更新(Optimistic Updates)
乐观更新是我觉得特别酷的功能。用户点击按钮,UI 立即变化,不用等待请求完成:
// 环境:React + React Query
// 场景:点赞功能的乐观更新
const likeMutation = useMutation({
mutationFn: likePost,
// 请求发出前:立即更新 UI
onMutate: async (postId) => {
await queryClient.cancelQueries({ queryKey: ['post', postId] });
const previousPost = queryClient.getQueryData(['post', postId]);
// 乐观更新:立即显示"已点赞"
queryClient.setQueryData(['post', postId], (old) => ({
...old,
liked: true,
likeCount: old.likeCount + 1,
}));
return { previousPost };
},
// 请求失败:回滚
onError: (err, postId, context) => {
queryClient.setQueryData(['post', postId], context.previousPost);
alert('点赞失败');
},
// 请求成功:确保数据最新
onSettled: (data, error, postId) => {
queryClient.invalidateQueries({ queryKey: ['post', postId] });
},
});
// 用户体验:
// 1. 点击点赞 → UI 立即变化(乐观更新) ✅
// 2. 后台发送请求
// 3a. 成功 → 保持已点赞状态
// 3b. 失败 → 回滚到未点赞,提示错误
对比 useState 的做法:
// useState:必须等请求成功才更新
const [liked, setLiked] = useState(false);
const handleLike = async () => {
try {
await likePost(postId);
setLiked(true); // 等请求成功才变化
} catch (err) {
alert('失败');
}
};
// 问题:用户点击 → 等待 → 才看到效果(体验差)
请求去重
React Query 还会自动去重请求:
// 环境:React + React Query
// 场景:多个组件同时需要相同数据
function Header() {
const { data: user } = useQuery({
queryKey: ['user', 123],
queryFn: () => fetchUser(123),
});
return <div>{user?.name}</div>;
}
function Sidebar() {
const { data: user } = useQuery({
queryKey: ['user', 123],
queryFn: () => fetchUser(123),
});
return <div>{user?.avatar}</div>;
}
// React Query 的行为:
// 1. Header 组件渲染 → 触发请求
// 2. Sidebar 组件渲染(几乎同时)
// 3. React Query 检测到:同样的 queryKey
// 4. 复用第一个请求的结果
// 5. 只发一次网络请求 ✅
// useState 的行为:
// 1. Header 和 Sidebar 各自独立
// 2. 发两次请求 ❌
SWR:更轻量的选择
说到服务端状态管理,还有一个不得不提的工具:SWR。
SWR 的核心理念
SWR 的名字来自 HTTP 缓存策略:Stale-While-Revalidate(过期时重新验证)。它的 API 非常简洁:
// 环境:React + SWR
// 场景:最简洁的服务端状态管理
// 依赖:npm install swr
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(r => r.json());
function UserProfile({ userId }) {
const { data, error, isLoading, mutate } = useSWR(
`/api/users/${userId}`, // key(同时也是 URL)
fetcher
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error</div>;
return <div>{data.name}</div>;
}
注意到了吗?SWR 的用法更简单,key 直接就是 URL。
与 React Query 对比
// React Query:更明确的配置
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 60000,
});
// SWR:更简洁,约定优于配置
const { data } = useSWR(`/api/users/${userId}`, fetcher);
SWR 的主要特性:
// 环境:React + SWR
// 场景:自动重新验证配置
// 自动重新验证
const { data } = useSWR('/api/user', fetcher, {
refreshInterval: 3000, // 每 3 秒刷新
revalidateOnFocus: true, // 窗口焦点时刷新
revalidateOnReconnect: true, // 网络恢复时刷新
});
// Mutation(更新数据)
import { mutate } from 'swr';
const updateUser = async (newData) => {
// 乐观更新
mutate('/api/user', newData, false);
// 发送请求
await fetch('/api/user', {
method: 'PUT',
body: JSON.stringify(newData),
});
// 重新验证
mutate('/api/user');
};
选择建议
我整理了这个对比表:
| 特性 | React Query | SWR |
|---|---|---|
| API 复杂度 | 稍复杂 | 非常简洁 ✅ |
| 功能完整度 | 更强大 ✅ | 够用 |
| 文件大小 | ~40KB | ~12KB ✅ |
| DevTools | 强大 ✅ | 基础 |
| 无限滚动 | 内置 ✅ | 需手动实现 |
| 学习曲线 | 陡 | 平缓 ✅ |
| 适用场景 | 复杂应用 | 简单应用 ✅ |
我的建议是:
选 SWR 的情况:
- ✅ 项目简单,主要是 GET 请求
- ✅ 追求代码简洁
- ✅ 使用 Next.js(同一作者 Vercel)
选 React Query 的情况:
- ✅ 复杂的数据交互
- ✅ 需要强大的 DevTools
- ✅ 需要高级功能(无限滚动、依赖查询)
如果让 AI 帮你写代码,SWR 的简单 API 可能更容易生成正确的代码。但如果你的应用比较复杂,React Query 的灵活性会更有价值。
实战场景
理论说了这么多,我们看几个实际场景。
场景 1:用户中心(数据共享)
问题:用户信息在多处使用,如何避免重复请求?
// 环境:React + React Query
// 场景:多个组件共享用户数据
// Header
function Header() {
const { data: user } = useQuery({
queryKey: ['currentUser'],
queryFn: fetchCurrentUser,
});
return <div>Hi, {user?.name}</div>;
}
// Sidebar
function Sidebar() {
const { data: user } = useQuery({
queryKey: ['currentUser'],
queryFn: fetchCurrentUser,
});
return <UserAvatar src={user?.avatar} />;
}
// Profile
function Profile() {
const { data: user } = useQuery({
queryKey: ['currentUser'],
queryFn: fetchCurrentUser,
});
return <UserDetails user={user} />;
}
// 结果:三个组件,只发一次请求 ✅
// useState 做不到这一点
场景 2:列表 + 详情(数据同步)
问题:详情页修改后,列表页如何同步?
// 环境:React + React Query
// 场景:列表和详情的数据同步
// 列表页
function PostList() {
const { data: posts } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
return posts?.map(post => <PostCard key={post.id} post={post} />);
}
// 详情页
function PostDetail({ postId }) {
const queryClient = useQueryClient();
const updateMutation = useMutation({
mutationFn: updatePost,
onSuccess: () => {
// 让列表页缓存失效
queryClient.invalidateQueries({ queryKey: ['posts'] });
// 列表页自动刷新 ✅
},
});
return <button onClick={() => updateMutation.mutate(newData)}>Update</button>;
}
场景 3:搜索(防抖 + 缓存)
问题:搜索框实时搜索,如何优化?
// 环境:React + React Query
// 场景:搜索功能的优化
// 依赖:npm install use-debounce
import { useDebounce } from 'use-debounce';
function SearchBox() {
const [keyword, setKeyword] = useState('');
const [debouncedKeyword] = useDebounce(keyword, 500);
const { data: results } = useQuery({
queryKey: ['search', debouncedKeyword],
queryFn: () => searchAPI(debouncedKeyword),
enabled: debouncedKeyword.length > 0,
staleTime: 5 * 60 * 1000, // 缓存 5 分钟
});
return (
<div>
<input
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
{results?.map(item => <SearchResult key={item.id} item={item} />)}
</div>
);
}
// 优势:
// - 输入 "react" → 等 500ms → 搜索
// - 再次搜索 "react" → 直接用缓存 ✅
// - 不需要手动管理缓存逻辑
场景 4:无限滚动
React Query 内置了无限滚动的支持:
// 环境:React + React Query
// 场景:无限滚动列表
import { useInfiniteQuery } from '@tanstack/react-query';
function InfinitePostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage) => lastPage.nextPage,
initialPageParam: 1,
});
return (
<div>
{data?.pages.map(page =>
page.posts.map(post => <PostCard key={post.id} post={post} />)
)}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : '加载更多'}
</button>
)}
</div>
);
}
最佳实践与常见陷阱
在使用 React Query 的过程中,我总结了一些经验。
Query Key 设计原则
// ❌ 不好:key 不够具体
useQuery({
queryKey: ['user'],
queryFn: fetchUser,
});
// ✅ 好:包含所有影响数据的参数
useQuery({
queryKey: ['user', userId, { includeProfile: true }],
queryFn: () => fetchUser(userId, { includeProfile: true }),
});
避免过度失效缓存
// ❌ 不好:让所有缓存失效
queryClient.invalidateQueries();
// ✅ 好:精确失效
queryClient.invalidateQueries({ queryKey: ['user', userId] });
// ✅ 更好:直接更新缓存(避免重新请求)
queryClient.setQueryData(['user', userId], newData);
常见陷阱
陷阱 1:忘记处理 undefined
// ❌ 错误
const { data } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
return <div>{data.name}</div>; // 报错!data 初始是 undefined
// ✅ 正确
return <div>{data?.name}</div>;
陷阱 2:缓存时间设置不当
// ❌ 不好:股票价格缓存 1 小时
const { data } = useQuery({
queryKey: ['stock'],
queryFn: fetchStock,
staleTime: 60 * 60 * 1000, // 数据会过期的!
});
// ✅ 好:根据数据特性设置
const { data } = useQuery({
queryKey: ['stock'],
queryFn: fetchStock,
staleTime: 1000, // 1秒后过期
refetchInterval: 5000, // 每5秒刷新
});
陷阱 3:在循环中使用 useQuery
// ❌ 错误:Hooks 不能在循环中
userIds.map(id => {
const { data } = useQuery({ queryKey: ['user', id], ... });
});
// ✅ 正确:使用 useQueries
const results = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
});
延伸与发散
写到这里,我又产生了一些新的思考:
与 Zustand 的配合
React Query 和 Zustand 各司其职:
// React Query:管理服务端数据
const { data: user } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
});
// Zustand:管理客户端状态
const useUIStore = create((set) => ({
sidebarOpen: false,
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
}));
// 各司其职,不要混用
SSR 中的使用
在 Next.js 等 SSR 框架中,React Query 也能很好地工作:
// 环境:Next.js + React Query
// 场景:服务端预取数据
export async function getServerSideProps({ params }) {
const queryClient = new QueryClient();
// 服务端预取数据
await queryClient.prefetchQuery({
queryKey: ['user', params.id],
queryFn: () => fetchUser(params.id),
});
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
}
// 客户端直接使用服务端数据
function UserPage({ id }) {
const { data: user } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
});
return <div>{user.name}</div>; // 首屏直接显示,无 loading
}
未来的思考
- React Server Components 会如何改变服务端状态管理?
- AI 流式输出适合用 React Query 吗?
- 大量缓存会导致内存问题吗?
- Suspense 模式的优缺点是什么?
这些问题我还在探索中,如果你有想法,欢迎交流。
小结
这篇文章从一个完整的对比开始,试图帮助你理解"为什么需要专门工具管理服务端数据"。
我的核心收获是:
- 服务端数据 ≠ 普通状态——它本质上是远程缓存
- useState 无法处理的问题:缓存、同步、失效、重试
- React Query/SWR 已经解决了这些问题,不需要重复造轮子
实用建议:
- 服务端数据 → React Query/SWR
- 客户端状态 → useState/Zustand
- 不要混用,分工明确
但我也想保持开放的态度。这些工具是否是"最佳实践"?在不同的项目规模、团队背景下,答案可能不同。重要的是理解背后的原理,而不是盲目跟风。
你之前是怎么管理服务端数据的?踩过哪些坑?准备尝试 React Query 吗?
参考资料
- TanStack Query 官方文档 - React Query 官方文档
- SWR 官方文档 - SWR 官方文档
- Practical React Query - TkDodo 的 React Query 实战系列
- React Query vs SWR - 两个工具的对比分析