为什么要用 TanStack Query 或 SWR 管理服务端状态?
服务端数据的真实来源在服务器,前端拿到的只是某一时刻的缓存副本,会过期、会被别人修改。TanStack Query 和 SWR 按 key 缓存请求结果,负责去重、过期判断、重新获取、重试和失效后刷新,组件只声明需要哪份数据。它们不适合用来存放纯客户端的界面状态。
简答
- 服务端状态与界面状态不同:数据由服务器持有,前端副本可能已经过期,需要决定何时重新获取,以及修改后哪些缓存要失效。
- 两个库都按 key 缓存:相同 key 的组件共享同一份数据和同一个进行中的请求,key 变化时自动请求新数据。
- TanStack Query 默认
staleTime为 0,数据一到手就视为过期;组件挂载、窗口重新聚焦、网络恢复时会在后台重新获取。没有组件使用的缓存 5 分钟后回收,失败的查询默认重试 3 次。 - SWR 按"先返回缓存,再重新验证"工作,窗口聚焦、网络恢复时可以重新验证,短时间内相同 key 的请求会去重。key 传
null时不发请求,用于条件请求。 - 修改数据后要让相关缓存失效:TanStack Query 用
queryClient.invalidateQueries,SWR 用mutate。
手写的问题
用 useEffect 加 useState 请求数据时,每个组件各自请求,多处使用同一接口会重复请求;切换参数时旧响应可能晚到并覆盖新数据;回到页面时数据不会刷新;修改之后要手动通知所有使用方。把这些逻辑放进全局 store 也只是换了地方写。
TanStack Query
import { keepPreviousData, useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function useTodos(filter) {
return useQuery({
queryKey: ['todos', filter],
queryFn: ({ signal }) =>
fetch(`/api/todos?filter=${encodeURIComponent(filter)}`, { signal }).then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}),
staleTime: 30_000, // 30 秒内视为新鲜,不因挂载或聚焦重新请求
placeholderData: keepPreviousData, // 切换 filter 时先显示上一份结果,避免闪回加载态
});
}
function useAddTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (todo) => {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(todo),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
// 前缀匹配:所有以 ['todos'] 开头的查询都会失效并重新获取
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
});
}
queryKey 要包含 queryFn 用到的所有参数,否则参数变化时会命中旧缓存。fetch 遇到 4xx/5xx 不会拒绝,要自己抛错,查询才会进入错误状态并触发重试。返回结果默认做结构共享,内容没变时保持原引用。
一个查询依赖另一个查询的结果时,用 enabled 控制何时执行:
const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail });
const userId = user?.id;
const { data: projects } = useQuery({
queryKey: ['projects', userId],
queryFn: () => getProjectsByUser(userId),
enabled: !!userId, // userId 就绪前不请求,状态为 pending、fetchStatus 为 idle
});
placeholderData: keepPreviousData 让新 key 的数据到达前继续显示上一次成功的结果,isPlaceholderData 可以区分当前是不是占位数据,适合分页和筛选切换。
SWR
import useSWR from 'swr';
const fetcher = async (url) => {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`); // 抛错后 SWR 才会进入 error 状态
return res.json();
};
function Profile({ userId }) {
// userId 为空时 key 为 null,不发请求
const { data, error, isLoading, mutate } = useSWR(userId ? `/api/users/${userId}` : null, fetcher);
// ...修改成功后调用 mutate() 重新验证这一个 key
}
useSWR 返回的 mutate 已绑定当前 key;要更新其他 key,用 useSWRConfig() 拿到全局 mutate 并传入 key。revalidateOnFocus、revalidateOnReconnect、dedupingInterval 等选项控制重新验证和去重,默认值以 SWR 官方 API 文档为准。
和全局 store 的分工
服务端数据交给查询库,弹窗开关、表单草稿、主题这类只存在于客户端的状态仍放在组件状态或 store 中,选择方法见《Zustand、Jotai 和 Redux Toolkit 应该怎样选择?》。
常见追问
staleTime和gcTime有什么区别?staleTime决定数据多久后需要重新获取,gcTime决定没有组件使用的缓存多久后从内存删除。- 怎样做乐观更新?在修改前写入预期结果,失败时回滚到之前的快照,成功或失败后都让缓存失效,以服务器数据为准。
- 快速切换参数会不会出现旧响应覆盖?查询库按 key 存放结果,旧 key 的响应写入旧 key 的缓存,不会显示到新 key 上。