TanStack Query vs SWR:React 数据获取选哪个

起因

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。

踩过的坑

  1. queryKey 写错:写 string 而非 array → cache 不 share 或者错
    share。永远 array:['posts', filter, page]

  2. fetchOptions vs queryFn:query 库不强制 fetch impl。
    传 axios / ofetch 都行,但要在 queryFn 返 promise。

  3. invalidation 过粗:mutation 后 invalidateQueries({ queryKey: ['posts'] }) invalidate 所有 posts*。可以更精确 exact: true

  4. stale closure in mutation:onSuccess 用 useState 值过时 →
    用 functional update 或 ref。

  5. SSR hydration mismatch:SSR data 跟 client first query 不一
    致 → flicker。两者都有 hydration helper(HydrationBoundary 等)。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。