
image
前置阅读: 这是本系列的最后一篇核心文章,强烈建议先读完前五篇:
本篇是对前面所有内容的整合------我们用React Query将之前手写的所有复杂逻辑浓缩成几行代码。
从100行代码到10行代码
某个创业团队的应用,用户管理界面需要:
go
需求清单:
✅ 加载用户列表
✅ 缓存用户数据
✅ 用户修改时更新缓存
✅ 支持分页
✅ 处理加载/错误状态
✅ 自动重试失败的请求
✅ 检测到window失焦时后台更新
✅ 防止重复请求
初级做法(自己写所有逻辑):
go
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [page, setPage] = useState(1);
const cache = useRef(newMap());
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const loadUsers = async () => {
const cacheKey = `page:${page}`;
if (cache.current.has(cacheKey)) {
setUsers(cache.current.get(cacheKey));
return;
}
try {
setLoading(true);
const response = await fetch(`/api/users?page=${page}`, {
signal: controller.signal
});
const data = await response.json();
if (isMounted) {
setUsers(data);
cache.current.set(cacheKey, data);
}
} catch (err) {
if (isMounted && err.name !== 'AbortError') {
setError(err.message);
}
} finally {
if (isMounted) setLoading(false);
}
};
loadUsers();
return() => {
isMounted = false;
controller.abort();
};
}, [page]);
// 添加用户
const addUser = async (userData) => {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(userData)
});
const newUser = await response.json();
// 手动更新缓存
setUsers([...users, newUser]);
return newUser;
};
// 处理window失焦
useEffect(() => {
const handleFocus = () => {
// 重新加载数据
loadUsers();
};
window.addEventListener('focus', handleFocus);
return() =>window.removeEventListener('focus', handleFocus);
}, []);
if (loading) return<div>加载中...</div>;
if (error) return<div>错误: {error}</div>;
return (
<div>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
<button onClick={() => setPage(p => p + 1)}>下一页</button>
</div>
);
}
代码行数:80+ 行,且容易出bug
用React Query做法:
go
function UserList() {
const [page, setPage] = useState(1);
const { data: users, isLoading, error } = useQuery({
queryKey: ['users', page],
queryFn: () => fetch(`/api/users?page=${page}`).then(r => r.json()),
});
if (isLoading) return<div>加载中...</div>;
if (error) return<div>错误: {error.message}</div>;
return (
<div>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
<button onClick={() => setPage(p => p + 1)}>下一页</button>
</div>
);
}
代码行数:15 行,而且自动处理了:
-
✅ 缓存
-
✅ window失焦后台更新
-
✅ 自动重试
-
✅ 防止重复请求
-
✅ 错误处理
代码减少:80行 → 15行(减少81%)
这不仅仅是少写代码,更重要的是少维护代码。React Query处理了所有的边界情况。
第一部分:为什么需要React Query
自己写缓存的5大问题
go
// 问题1:缓存永不过期
const cache = newMap();
cache.set('users', data); // 什么时候删除?
// 问题2:缓存失效复杂
function updateUser(userId, updates) {
// 需要手动清除相关缓存
cache.delete('users');
cache.delete(`user:${userId}`);
cache.delete('users:list');
// 还有其他地方用到这数据吗?我不确定...
}
// 问题3:防止重复请求需要自己写
let userRequestPromise = null;
function getUser(id) {
if (userRequestPromise) return userRequestPromise;
userRequestPromise = fetch(`/api/users/${id}`);
return userRequestPromise;
}
// 这样写每个API都要重复...
// 问题4:window失焦后更新需要监听
useEffect(() => {
const handleFocus = () => {
// 重新加载?但要避免不必要的请求...
};
window.addEventListener('focus', handleFocus);
return() =>window.removeEventListener('focus', handleFocus);
}, []);
// 问题5:分页缓存很容易混乱
// page=1的数据被page=2的请求覆盖了吗?
// 用户返回到page=1,需要重新加载吗?
React Query解决的问题
go
// ✅ 问题1:内置TTL机制
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5分钟
cacheTime: 10 * 60 * 1000// 10分钟后清除
}
}
});
// ✅ 问题2:智能缓存失效
useMutation({
mutationFn: updateUser,
onSuccess: () => {
// 一行代码,自动更新相关缓存
queryClient.invalidateQueries({ queryKey: ['users'] });
}
});
// ✅ 问题3:自动防重复
// 多个组件同时请求相同数据 → 只发一个请求
// ✅ 问题4:自动window focus重新获取
// 配置一行代码就自动处理
// ✅ 问题5:分页缓存自动隔离
useQuery({ queryKey: ['users', page] }); // 不同page自动独立缓存
第二部分:React Query基础
安装和初始化
go
npm install @tanstack/react-query
go
// main.jsx
import { QueryClient, QueryClientProvider } from'@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// ⏱️ 数据新鲜度时间
// 这段时间内,如果有缓存就直接用,不发新请求
staleTime: 5 * 60 * 1000, // 5分钟
// 💾 缓存保留时间
// 超过这个时间,缓存被删除
gcTime: 10 * 60 * 1000, // 10分钟(新版本叫gcTime,旧版本叫cacheTime)
// 🔄 失败重试
// 失败的请求自动重试次数
retry: 1,
// ⚡ 是否在window获焦时重新获取
refetchOnWindowFocus: true,
// 📡 是否在重新mount时重新获取
refetchOnMount: true,
},
},
});
exportdefaultfunction App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
你的第一个Query
go
// hooks/useUsers.js
import { useQuery } from'@tanstack/react-query';
exportfunction useUsers(page = 1) {
return useQuery({
// 查询键:用于缓存标识和失效
// 不同的queryKey = 不同的缓存条目
queryKey: ['users', { page }],
// 查询函数:实际的API调用
// 接收一个signal用于cancellation
queryFn: async ({ signal }) => {
const response = await fetch(`/api/users?page=${page}`, { signal });
if (!response.ok) {
thrownewError(`API error: ${response.status}`);
}
return response.json();
},
// staleTime可以按查询覆盖全局设置
staleTime: 5 * 60 * 1000,
});
}
// components/UserList.jsx
import { useUsers } from'../hooks/useUsers';
function UserList() {
const [page, setPage] = useState(1);
// 返回值包含所有你需要的状态
const {
data: users, // 实际的数据
isLoading, // 首次加载中
isPending, // 加载中(包括background refetch)
isFetching, // 正在后台获取
error, // 错误对象
status, // 'pending' | 'error' | 'success'
fetchStatus, // 'idle' | 'fetching' | 'paused'
} = useUsers(page);
if (isLoading) {
return<div className="loading">加载用户列表中...</div>;
}
if (error) {
return<div className="error">错误: {error.message}</div>;
}
// 后台更新时显示指示器
if (isFetching && !isLoading) {
return<div className="bg-sync">💫 更新中...</div>;
}
return (
<div>
<ul>
{users?.map(user => (
<li key={user.id}>
{user.name}
{user.email && <span> ({user.email})</span>}
</li>
))}
</ul>
<div className="pagination">
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
>
← 上一页
</button>
<span>第 {page} 页</span>
<button onClick={() => setPage(p => p + 1)}>
下一页 →
</button>
</div>
</div>
);
}
第三部分:查询键的艺术
查询键决定了缓存的范围。这是React Query中最容易被误用的概念。
查询键的原则
go
// ❌ 错误:太通用
useQuery({
queryKey: ['data'], // 太宽泛,什么数据?
queryFn: () => fetch('/api/users').then(r => r.json())
});
// ✅ 正确:具体和层级化
useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json())
});
// ❌ 错误:把参数写成字符串
useQuery({
queryKey: [`users:${userId}`], // 还是字符串拼接,丑
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json())
});
// ✅ 正确:参数作为数组元素
useQuery({
queryKey: ['users', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json())
});
// ✅ 更好:参数作为对象(便于失效时匹配)
useQuery({
queryKey: ['users', { id: userId, include: 'posts' }],
queryFn: ({ queryKey }) => {
const [, { id, include }] = queryKey;
return fetch(`/api/users/${id}?include=${include}`).then(r => r.json());
}
});
// ✅ 层级化键:用于精确失效
useQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => fetch(`/api/users/${userId}/posts`).then(r => r.json())
});
// 当用户更新时:
queryClient.invalidateQueries({
queryKey: ['users', userId] // 会失效 ['users', userId] 和 ['users', userId, 'posts']
});
查询键的失效模式
go
// 场景:用户修改了信息
const updateUser = useMutation({
mutationFn: (updates) => patch(`/api/users/${userId}`, updates),
onSuccess: () => {
// 模式1:精确失效
queryClient.invalidateQueries({
queryKey: ['users', userId]
});
// 模式2:使用前缀失效(所有相关的缓存)
queryClient.invalidateQueries({
queryKey: ['users'],
exact: false// 匹配所有以['users']开头的键
});
// 模式3:使用predicate失效
queryClient.invalidateQueries({
predicate: (query) => {
return query.queryKey[0] === 'users'; // 手动指定条件
}
});
}
});
第四部分:依赖查询
某些数据依赖于其他数据。React Query用enabled选项优雅地处理这个问题。
go
// 用户选择一个作者,然后加载他的文章
function AuthorPosts({ authorId }) {
// 第一步:加载作者信息
const {
data: author,
isLoading: authorLoading,
} = useQuery({
queryKey: ['authors', authorId],
queryFn: () => fetch(`/api/authors/${authorId}`).then(r => r.json()),
});
// 第二步:只有当作者加载完毕,才加载他的文章
const {
data: posts,
isLoading: postsLoading,
} = useQuery({
queryKey: ['authors', authorId, 'posts'],
queryFn: () => fetch(`/api/authors/${authorId}/posts`).then(r => r.json()),
enabled: !!author, // ✅ 关键:只在author存在时执行
});
if (authorLoading) return<div>加载作者...</div>;
if (!author) return<div>作者不存在</div>;
if (postsLoading) return<div>加载文章...</div>;
return (
<div>
<h1>{author.name}</h1>
<ul>
{posts?.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
第五部分:使用useMutation修改数据
Reading是Query,Writing是Mutation。
go
// 完整的添加用户示例
function AddUserForm() {
const queryClient = useQueryClient();
const [formData, setFormData] = useState({ name: '', email: '' });
// 定义mutation
const mutation = useMutation({
// mutationFn:实际的API调用
mutationFn: async (userData) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
});
if (!response.ok) {
thrownewError('添加用户失败');
}
return response.json();
},
// onMutate:mutation开始前运行(用于乐观更新)
onMutate: async (userData) => {
// 取消任何pending的查询
await queryClient.cancelQueries({ queryKey: ['users'] });
// 保存旧数据(用于回滚)
const previousUsers = queryClient.getQueryData(['users']);
// 乐观更新(立即显示新用户)
queryClient.setQueryData(['users'], (old) => {
return [...(old || []), { ...userData, id: Date.now() }];
});
return { previousUsers };
},
// onSuccess:mutation成功
onSuccess: (newUser) => {
// 重新获取用户列表以获取完整数据(id、创建时间等)
queryClient.invalidateQueries({ queryKey: ['users'] });
},
// onError:mutation失败
onError: (error, variables, context) => {
// 回滚乐观更新
if (context?.previousUsers) {
queryClient.setQueryData(['users'], context.previousUsers);
}
// 显示错误提示
console.error('失败:', error.message);
},
// onSettled:无论成功或失败都运行
onSettled: () => {
// 清空表单
setFormData({ name: '', email: '' });
},
});
const handleSubmit = (e) => {
e.preventDefault();
mutation.mutate(formData);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="用户名"
required
/>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="邮箱"
required
/>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? '添加中...' : '添加用户'}
</button>
{/* 状态反馈 */}
{mutation.isPending && <p>⏳ 正在添加...</p>}
{mutation.isSuccess && <p>✅ 添加成功!</p>}
{mutation.isError && (
<p style={{ color: 'red' }}>❌ 添加失败: {mutation.error.message}</p>
)}
</form>
);
}
第六部分:分页
偏移分页(Offset-Based)
go
// 传统的"第1页、第2页"分页
function PaginatedUserList() {
const [page, setPage] = useState(1);
const PAGE_SIZE = 20;
const {
data,
isLoading,
isPreviousData, // 上一页数据是否还在显示
} = useQuery({
queryKey: ['users', page],
queryFn: () =>
fetch(`/api/users?page=${page}&limit=${PAGE_SIZE}`).then(r => r.json()),
// keepPreviousData在加载时显示旧数据
placeholderData: (previousData) => previousData,
});
const maxPages = Math.ceil((data?.total || 0) / PAGE_SIZE);
return (
<div>
{isPreviousData && <div className="stale">(显示旧数据)</div>}
<ul>
{data?.users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
<div className="pagination">
<button onClick={() => setPage(1)} disabled={page === 1}>
首页
</button>
<button
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
>
上一页
</button>
<span>第 {page} / {maxPages} 页</span>
<button
onClick={() => setPage(p => p + 1)}
disabled={page >= maxPages}
>
下一页
</button>
<button onClick={() => setPage(maxPages)} disabled={page >= maxPages}>
末页
</button>
</div>
</div>
);
}
无限滚动(Infinite Scroll)
go
// "滚动加载更多"式分页
import { useInfiniteQuery } from'@tanstack/react-query';
import { useInView } from'react-intersection-observer';
function InfiniteUserList() {
// useInfiniteQuery:为无限滚动设计
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['users:infinite'],
// queryFn接收pageParam
queryFn: async ({ pageParam = 0 }) => {
const response = await fetch(
`/api/users?cursor=${pageParam}&limit=20`
);
return response.json();
},
// 指定如何获取下一页的cursor
getNextPageParam: (lastPage) => {
return lastPage.nextCursor; // undefined = 没有更多数据
},
// 初始的cursor值
initialPageParam: 0,
});
// 使用Intersection Observer检测用户滚动
const { ref, inView } = useInView();
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage]);
if (status === 'pending') return<div>加载中...</div>;
if (status === 'error') return<div>加载失败</div>;
return (
<div>
{/* 渲染所有页面的数据 */}
{data?.pages.map((page, pageIndex) => (
<div key={pageIndex}>
{page.users?.map(user => (
<div key={user.id} className="user-item">
{user.name}
</div>
))}
</div>
))}
{/* 加载触发器 */}
<div ref={ref} className="load-more-trigger">
{isFetchingNextPage ? (
'⏳ 加载更多中...'
) : hasNextPage ? (
'👇 滚动加载更多'
) : (
'✅ 没有更多数据了'
)}
</div>
</div>
);
}
第七部分:React Query vs 自己写的对比
代码行数对比
go
任务:完整的用户管理(读+写+缓存+分页)
自己写:
├─ useEffect获取数据 30行
├─ 缓存管理 40行
├─ 失效和更新 50行
├─ 分页逻辑 30行
├─ 加载/错误状态 20行
└─ 总计 170行 ❌
React Query:
├─ useQuery 5行
├─ useMutation 10行
├─ useInfiniteQuery 8行
└─ 总计 23行 ✅
代码减少:170 → 23 = 减少87%
功能对比
| 功能 | 自己写 | React Query |
|---|---|---|
| 缓存 | ⚠️ 复杂 | ✅ 内置 |
| 缓存失效 | ⚠️ 容易漏 | ✅ 自动 |
| 防重复请求 | ⚠️ 要写 | ✅ 自动 |
| Window focus更新 | ⚠️ 要写 | ✅ 内置 |
| 乐观更新 | ❌ 很难 | ✅ 容易 |
| 分页 | ⚠️ 易出bug | ✅ 成熟 |
| 加载状态 | ⚠️ 要管理 | ✅ 自动 |
| 错误处理 | ⚠️ 要自己处理 | ✅ 内置 |
| 重试逻辑 | ❌ 没有 | ✅ 内置 |
| DevTools调试 | ❌ 没有 | ✅ 官方工具 |
第八部分:生产级的React Query配置
go
// react-query-config.js
import { QueryClient } from'@tanstack/react-query';
import { createSyncStoragePersister } from'@tanstack/query-sync-storage-persister';
import { persistQueryClient } from'@tanstack/react-query-persist-client';
exportconst queryClient = new QueryClient({
defaultOptions: {
queries: {
// 数据新鲜度
staleTime: 5 * 60 * 1000, // 5分钟内认为数据新鲜
gcTime: 10 * 60 * 1000, // 10分钟后清除未使用的缓存
// 重试策略
retry: (failureCount, error) => {
// 4xx错误不重试
if (error.status >= 400 && error.status < 500) {
returnfalse;
}
// 5xx错误重试,最多3次
return failureCount < 3;
},
retryDelay: (attemptIndex) => {
// 指数退避:100ms, 200ms, 400ms
returnMath.min(1000 * 2 ** attemptIndex, 30000);
},
// 自动更新
refetchOnWindowFocus: true,
refetchOnMount: 'stale', // 仅当数据过期时
refetchOnReconnect: true, // 网络恢复时
},
},
});
// 可选:持久化缓存到localStorage
const localStoragePersister = createSyncStoragePersister({
storage: window.localStorage,
});
persistQueryClient({
queryClient,
persister: localStoragePersister,
maxAge: 24 * 60 * 60 * 1000, // 24小时
});
总结:从自己写到用框架
学习曲线
go
简单性:
自己写所有 ← → 用React Query
❌ 简单 ✅ 更简单
功能完整性:
自己写 ← → 用React Query
⚠️ 容易漏 ✅ 完整
可维护性:
自己写 ← → 用React Query
❌ 很难 ✅ 容易
性能:
自己写 ← → 用React Query
❌ 容易优化失败 ✅ 优化好的
何时用React Query
| 场景 | 建议 |
|---|---|
| 简单的一次性fetch | ❌ 不需要 |
| 有缓存需求 | ✅ 必用 |
| 有分页需求 | ✅ 必用 |
| 需要乐观更新 | ✅ 推荐 |
| 大型应用 | ✅ 必用 |
| 频繁的数据更新 | ✅ 必用 |
React Query的一句话
React Query是数据同步层,让你的React应用自动和服务器保持同步,不用手写缓存逻辑。
最后的话
这一篇是一个重要的分水岭------从"自己管理所有逻辑"到"用框架做正确的事"。
很多初级开发者被困在自己写缓存的泥沼里,而不知道React Query这样的库能简化90%的工作。
掌握了这一篇的内容,你就能:
-
✅ 减少项目代码量50-80%
-
✅ 避免90%的缓存相关bug
-
✅ 自动获得最佳实践
-
✅ 有更多精力关注业务逻辑
点赞、分享、评论支持,下一篇见!