TanStack Query 服务端状态管理
TanStack Query 的核心用法:useQuery 缓存 API 数据、useMutation 处理写操作、queryKey 设计、乐观更新、与 Zustand 的分工。
#type / howto
#status / growing
#tech / dev / frontend
#resource / react
[!info] related notes
- 并列: Zustand 全局状态
- 实践: BodySense 项目 MOC
TanStack Query 服务端状态管理
核心问题
从 API 获取的数据有独特的生命周期问题:数据可能过期、可能被其他客户端修改、需要缓存避免重复请求、需要 loading/error 状态。用 useEffect + useState 手动管理这些问题会导致大量样板代码和 bug。
核心概念
queryKey — 缓存键
useQuery({ queryKey: ['consultations'], queryFn: fetchList });
useQuery({ queryKey: ['consultation', id], queryFn: () => fetchOne(id) });
- 相同 key 共享缓存
- key 变化时自动重新获取
- key 要包含所有影响结果的参数
staleTime — 数据新鲜度
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
staleTime: 5 * 60 * 1000, // 5 分钟内不重新请求
});
- 0(默认):每次组件挂载都重新请求
- Infinity:永远不自动重新请求(手动 invalidate)
- 中间值:窗口期内用缓存,过期后后台刷新
useMutation — 写操作
const mutation = useMutation({
mutationFn: (message: string) => sendMessage(sessionId, message),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['consultation', sessionId] });
},
});
写操作后使相关缓存失效,触发重新获取。
queryKey 设计原则
// ✅ 包含所有影响结果的参数
useQuery({ queryKey: ['users', { page, limit, role }], queryFn: ... });
// ❌ 参数变化但 key 没变,不会重新获取
useQuery({ queryKey: ['users'], queryFn: () => fetchUsers(page, limit) });
乐观更新
先更新 UI,等服务器确认(或回滚):
const mutation = useMutation({
mutationFn: updateProfile,
onMutate: async (newData) => {
await queryClient.cancelQueries({ queryKey: ['profile'] });
const previous = queryClient.getQueryData(['profile']);
queryClient.setQueryData(['profile'], newData); // 立即更新
return { previous };
},
onError: (err, newData, context) => {
queryClient.setQueryData(['profile'], context.previous); // 回滚
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['profile'] }); // 最终同步
},
});
与 Zustand 的分工
Zustand: 认证 token、UI 状态、表单草稿
TanStack Query: 会话列表、评估报告、训练计划、用户档案
简单判断:数据来自 API → TanStack Query;数据在客户端产生 → Zustand。
常见错误
useEffect + useState 手动 fetch
// ❌ 手动管理
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData);
}, []);
// ✅ 用 useQuery
const { data } = useQuery({ queryKey: ['data'], queryFn: () => fetch('/api/data').then(r => r.json()) });
不处理错误
// ❌ 忽略错误
const { data } = useQuery({ ... });
// ✅ 处理错误
const { data, error, isLoading } = useQuery({ ... });
if (error) return <ErrorMessage error={error} />;