DevToolBox免费
博客

React Query 模式 2026:TanStack Query 数据获取、缓存与变更

13 分钟作者 DevToolBox

TanStack Query(前身为 React Query)是 React 应用中服务端状态管理的标准解决方案。它处理缓存、后台重新获取、去重、分页和无限查询——所有这些都通过简单的基于 hooks 的 API 实现。

useQuery:获取和缓存数据

useQuery 订阅服务端状态。queryKey 是缓存键——唯一标识数据的任何数组。staleTime 控制数据何时被视为过期并重新获取。

import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';

// Setup: wrap your app
const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            staleTime: 1000 * 60 * 5,  // 5 minutes
            gcTime: 1000 * 60 * 30,     // 30 minutes (formerly cacheTime)
            retry: 3,
            refetchOnWindowFocus: true,
        },
    },
});

function App() {
    return (
        <QueryClientProvider client={queryClient}>
            <MyApp />
        </QueryClientProvider>
    );
}

// Basic query
interface User {
    id: number;
    name: string;
    email: string;
}

function useUser(userId: number) {
    return useQuery<User>({
        queryKey: ['user', userId],      // cache key — must be unique
        queryFn: async () => {
            const res = await fetch(`/api/users/${userId}`);
            if (!res.ok) throw new Error('Failed to fetch user');
            return res.json();
        },
        enabled: userId > 0,            // only run if userId is valid
        staleTime: 1000 * 60 * 10,     // override global default
    });
}

function UserProfile({ userId }: { userId: number }) {
    const { data, isLoading, isError, error } = useUser(userId);

    if (isLoading) return <div>Loading...</div>;
    if (isError) return <div>Error: {error.message}</div>;

    return <div>{data.name} — {data.email}</div>;
}

useMutation:创建、更新、删除

useMutation 处理创建/更新/删除操作。onSuccess 回调让你无效化相关查询或直接更新缓存而无需重新获取。

import { useMutation, useQueryClient } from '@tanstack/react-query';

interface CreatePostInput {
    title: string;
    body: string;
    userId: number;
}

function useCreatePost() {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: async (input: CreatePostInput) => {
            const res = await fetch('/api/posts', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(input),
            });
            if (!res.ok) throw new Error('Failed to create post');
            return res.json();
        },
        onSuccess: (newPost) => {
            // Invalidate and refetch posts list
            queryClient.invalidateQueries({ queryKey: ['posts'] });
            // Or add directly to cache (no refetch needed)
            queryClient.setQueryData(['post', newPost.id], newPost);
        },
        onError: (error) => {
            console.error('Failed to create post:', error);
        },
    });
}

function CreatePostForm() {
    const mutation = useCreatePost();

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        mutation.mutate({ title: 'New Post', body: 'Content', userId: 1 });
    };

    return (
        <form onSubmit={handleSubmit}>
            <button type="submit" disabled={mutation.isPending}>
                {mutation.isPending ? 'Creating...' : 'Create Post'}
            </button>
            {mutation.isError && <p>Error: {mutation.error.message}</p>}
            {mutation.isSuccess && <p>Post created!</p>}
        </form>
    );
}

乐观更新实现即时 UI

乐观更新在服务器响应之前立即将更改应用于 UI。如果请求失败,onError 回调会回滚到之前的状态。

import { useMutation, useQueryClient } from '@tanstack/react-query';

interface Todo {
    id: number;
    title: string;
    completed: boolean;
}

function useToggleTodo() {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: (todo: Todo) =>
            fetch(`/api/todos/${todo.id}`, {
                method: 'PATCH',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ completed: !todo.completed }),
            }).then(r => r.json()),

        // Optimistic update — runs BEFORE the mutation
        onMutate: async (updatedTodo) => {
            // Cancel outgoing refetches
            await queryClient.cancelQueries({ queryKey: ['todos'] });

            // Snapshot current data for rollback
            const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);

            // Optimistically update the cache
            queryClient.setQueryData<Todo[]>(['todos'], (old) =>
                old?.map(todo =>
                    todo.id === updatedTodo.id
                        ? { ...todo, completed: !todo.completed }
                        : todo
                )
            );

            return { previousTodos }; // context for onError
        },

        // If mutation fails, roll back to snapshot
        onError: (_err, _variables, context) => {
            if (context?.previousTodos) {
                queryClient.setQueryData(['todos'], context.previousTodos);
            }
        },

        // Always refetch after error or success
        onSettled: () => {
            queryClient.invalidateQueries({ queryKey: ['todos'] });
        },
    });
}

分页的无限查询

useInfiniteQuery 处理基于游标和偏移量的分页。getNextPageParam 从每个页面响应中提取下一页游标。

import { useInfiniteQuery } from '@tanstack/react-query';

interface Page {
    items: Post[];
    nextCursor?: string;
}

function useInfinitePosts() {
    return useInfiniteQuery<Page>({
        queryKey: ['posts', 'infinite'],
        queryFn: async ({ pageParam }) => {
            const url = pageParam
                ? `/api/posts?cursor=${pageParam}`
                : '/api/posts';
            return fetch(url).then(r => r.json());
        },
        initialPageParam: undefined as string | undefined,
        getNextPageParam: (lastPage) => lastPage.nextCursor,
    });
}

function InfinitePostList() {
    const {
        data,
        fetchNextPage,
        hasNextPage,
        isFetchingNextPage,
    } = useInfinitePosts();

    return (
        <div>
            {data?.pages.flatMap(page => page.items).map(post => (
                <div key={post.id}>{post.title}</div>
            ))}
            {hasNextPage && (
                <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
                    {isFetchingNextPage ? 'Loading...' : 'Load more'}
                </button>
            )}
        </div>
    );
}

查询键模式和预获取

一致的查询键工厂防止缓存不匹配。悬停时预获取创造即时页面转换——用户点击时数据已在缓存中。

// Query key patterns — factory functions for consistency
const queryKeys = {
    all: ['posts'] as const,
    lists: () => [...queryKeys.all, 'list'] as const,
    list: (filters: string) => [...queryKeys.lists(), { filters }] as const,
    details: () => [...queryKeys.all, 'detail'] as const,
    detail: (id: number) => [...queryKeys.details(), id] as const,
};

// Usage:
useQuery({ queryKey: queryKeys.detail(1), queryFn: ... });

// Invalidate all posts:
queryClient.invalidateQueries({ queryKey: queryKeys.all });

// Invalidate only lists:
queryClient.invalidateQueries({ queryKey: queryKeys.lists() });

// Prefetching for better UX
async function prefetchUser(userId: number) {
    await queryClient.prefetchQuery({
        queryKey: ['user', userId],
        queryFn: () => fetchUser(userId),
        staleTime: 10 * 60 * 1000, // only prefetch if > 10 min stale
    });
}

// On hover: prefetch before click
<link onMouseEnter={() => prefetchUser(userId)} href={`/users/${userId}`}>
    View Profile
</link>

TanStack Query vs 其他解决方案

FeatureTanStack QueryRedux ToolkitSWRApollo
CachingBuilt-in (staleTime, gcTime)ManualBuilt-inBuilt-in (Apollo-only)
Optimistic updatesFirst-classManualBasicFirst-class
Infinite queryuseInfiniteQueryManualuseSWRInfinitefetchMore
MutationsuseMutationRTK QueryNo (use fetch)useMutation
DevToolsExcellentExcellentBasicGood
REST + GraphQLBothBothREST focusedGraphQL only

最佳实践

  • 使用查询键工厂(带函数的对象)确保一致性并实现精确缓存失效。
  • 根据数据新鲜度需求设置 staleTime。用户资料:10 分钟。股票价格:0 秒。参考数据:1 小时。
  • 始终提供加载和错误状态。使用 React Suspense 模式配合 Suspense 边界编写更简洁的代码。
  • 对切换操作、点赞等快速 mutations 使用乐观更新——使应用感觉即时响应。
  • 在悬停或路由切换时预获取数据。TanStack Query 的 prefetchQuery 是幂等的——可以多次安全调用。

常见问题

staleTime 和 gcTime 有什么区别?

staleTime 控制缓存数据被认为是新鲜的时长(不需要重新获取)。gcTime 控制非活动缓存条目在垃圾回收前在内存中保存多长时间。

TanStack Query 如何处理竞态条件?

TanStack Query 自动去重相同的请求——如果 10 个组件同时请求同一查询键,只会触发 1 个网络请求。响应是共享的。

我应该将 TanStack Query 与 Redux 或 Zustand 一起使用吗?

TanStack Query 处理服务端状态(远程数据);Redux/Zustand 处理客户端状态(UI 状态、选择、表单草稿)。它们相互补充。

如何全局处理认证错误?

使用 QueryClient 的全局 onError 回调或自定义查询函数包装器。对于 401 错误,重定向到登录。

TanStack Query v5 有什么变化?

v5 将 cacheTime 重命名为 gcTime,使查询函数参数变为对象,删除了 useQuery 中的成功/错误/结算回调,并添加了对 React Server Components 的 SSR 支持。

相关工具

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

{ }JSON FormatterB→Base64 Encode OnlineIDUUID Generator Online

相关文章

React Server Components 完全指南 2026

掌握 React Server Components:架构、数据获取、流式传输和迁移策略。

TypeScript 类型守卫:运行时类型检查完全指南

掌握 TypeScript 类型守卫:typeof、instanceof、in、自定义类型守卫和可辨识联合。

Next.js App Router: 2026 完整迁移指南

掌握 Next.js App Router 的全面指南。学习 Server Components、数据获取、布局、流式渲染、Server Actions,以及从 Pages Router 的逐步迁移策略。