起因
React 组件里调 API 老方法:
useEffect(() => {
setLoading(true);
fetch('/api/posts')
.then(r => r.json())
.then(setPosts)
.finally(() => setLoading(false));
}, []);
- 没 cache,组件 mount 都重 fetch
- 没 retry / refetch
- 没共享:两个组件用同 endpoint 各 fetch 一次
- 没 stale-while-revalidate
- 错误处理累
TanStack Query (前 React Query) / SWR 解决这些。
SWR (Vercel)
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(r => r.json());
function Posts() {
const { data, error, isLoading } = useSWR('/api/posts', fetcher);
if (isLoading) return <Spinner />;
if (error) return <Error />;
return <List items={data} />;
}
简单 API:URL = cache key,fetcher 函数任意。
优势
- API 极简
- bundle 小(4 KB)
- Vercel 友好(Next.js 推荐)
- focus revalidation 默认(窗口聚焦自动 refetch)
劣势
- mutation API 较弱
- 复杂场景能力上限低
- 文档比 TanStack Query 少
TanStack Query
import { useQuery } from '@tanstack/react-query';
function Posts() {
const { data, error, isLoading } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(r => r.json()),
});
if (isLoading) return <Spinner />;
if (error) return <Error />;
return <List items={data} />;
}
API 类似但更"框架化"。
优势
- 功能极完整(pagination / infinite / mutation / optimistic update)
- devtools 强(专门 query inspector)
- 框架无关(react / vue / solid / svelte 都有版本)
- 大型项目首选
劣势
- bundle 大(13 KB gzip)
- 概念多(queryClient / mutation / invalidation)
- 学习曲线高于 SWR
共享 cache
两者都按 key cache:
// 组件 A
useSWR('/api/posts');
// 组件 B(同 URL)
useSWR('/api/posts');
// → 只 fetch 一次,共享 data
TanStack Query 同理(按 queryKey)。
跨组件状态共享解决了,不需要 Redux 把 server state 塞 store。
stale-while-revalidate
1. mount → 显 cached data (instant)
2. 后台 refetch
3. 新 data 到 → 静默 update
用户感觉"立刻有数据" + 后台保持最新。
两者都默认 SWR 行为。
可配 staleTime:< staleTime 不 refetch(节省请求):
useQuery({
queryKey: ['posts'],
queryFn,
staleTime: 5 * 60 * 1000, // 5 min 内不重新 fetch
});
mutation (TanStack)
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newPost) => fetch('/api/posts', { method: 'POST', body: JSON.stringify(newPost) }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
function NewPostForm() {
return <button onClick={() => mutation.mutate({ title: 'hi' })}>Save</button>;
}
mutation 完后 invalidate ['posts'] query → 自动 refetch。
optimistic update
useMutation({
mutationFn: deletePost,
onMutate: async (postId) => {
await queryClient.cancelQueries({ queryKey: ['posts'] });
const prev = queryClient.getQueryData(['posts']);
queryClient.setQueryData(['posts'], (old) => old.filter(p => p.id !== postId));
return { prev };
},
onError: (err, postId, ctx) => {
queryClient.setQueryData(['posts'], ctx.prev); // rollback
},
});
UI 立刻显示删除 → API 失败 → 自动 rollback。
专业级 UX,TanStack Query 内置 helper。
SWR 也能做但要手动。
pagination
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) => fetch(`/api/posts?page=${pageParam}`).then(r => r.json()),
getNextPageParam: (lastPage) => lastPage.next_page,
initialPageParam: 1,
});
fetchNextPage 加载下一页,data 累积。
infinite scroll 一行代码。
SWR 的 useSWRInfinite 类似但 API 略别扭。
prefetching
// hover 时预拉数据
function PostLink({ postId }) {
const queryClient = useQueryClient();
return (
<a
onMouseEnter={() => queryClient.prefetchQuery({
queryKey: ['post', postId],
queryFn: () => fetch(`/api/posts/${postId}`).then(r => r.json()),
})}
>
...
</a>
);
}
用户点链接前已经预 fetch → 点击 = 立刻有数据。
devtools
TanStack Query devtools:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
<>
<App />
<ReactQueryDevtools />
</>
floating 浮窗看所有 active query / state / cache / refetch。
debug 神器。
SWR devtools 弱很多。
选择
- 简单项目 / 几个 query → SWR
- 复杂应用 / 多 mutation / 需 devtools → TanStack Query
- Next.js + SWR 风格 → SWR
- 跨框架 → TanStack Query(React/Vue/Svelte/Solid 都行)
我的默认是 TanStack Query(功能完整 + 不会后悔)。
与 RTK Query 对比
RTK Query (Redux Toolkit):
- Redux 圈子的查询库
- 跟 Redux store 集成深
- 学习曲线更陡
已经用 Redux → RTK Query 自然。
没用 Redux → TanStack Query / SWR 更轻。
Server Components 时代
React 19 + Next.js App Router 倾向 Server Components 直接 fetch:
// Server Component
async function Posts() {
const posts = await fetch('https://...').then(r => r.json());
return <List items={posts} />;
}
server-side 数据获取不需要 query 库。
但 client-side mutation / interactive 数据还是需要 TanStack Query / SWR。
混用:server fetch 初始 → client query 后续更新。
真实 case
我们一个 dashboard 用 TanStack Query:
- 20+ query (各种 metric / list)
- 10+ mutation
- 大量 prefetch (hover 看 detail)
- 自动 refetch on window focus(用户回来看最新)
效果:
- delete useEffect / useState 50%
- 用户体验显著(看不到 loading spinner,always cached)
- bundle +13 KB but worth
之前手写 useEffect + Context:500 行 boilerplate。
TanStack Query:100 行 query / mutation definition。
踩过的坑
-
queryKey 写错:写 string 而非 array → cache 不 share 或者错
share。永远 array:['posts', filter, page]。 -
fetchOptions vs queryFn:query 库不强制 fetch impl。
传 axios / ofetch 都行,但要在 queryFn 返 promise。 -
invalidation 过粗:mutation 后
invalidateQueries({ queryKey: ['posts'] })invalidate 所有 posts*。可以更精确exact: true。 -
stale closure in mutation:onSuccess 用 useState 值过时 →
用 functional update 或 ref。 -
SSR hydration mismatch:SSR data 跟 client first query 不一
致 → flicker。两者都有 hydration helper(HydrationBoundary 等)。
登录后参与评论。