人人都会AI编程

14.2 TanStack Query(React Query)

更新时间:2026-07-10

TanStack Query(原名 React Query)是一个服务端状态管理库,它不负责管理客户端状态(如表单输入),而是专门解决服务端数据的获取、缓存、同步与更新。你可以把它理解为前端与后端之间的一层“智慧缓存层”,让数据请求变得更简单、更可靠。

它的核心价值在于:把需要大量手写逻辑处理的异步状态(加载中、错误、重试、缓存时效、后台刷新、分页、乐观更新)标准化,让你只需声明数据从哪来,其余都由 Query 自行管理。


14.2.1 核心能力概览

  • 数据缓存:请求结果自动按查询键(Query Key)缓存,相同查询不重复发出网络请求,直接返回缓存。
  • 自动重取:数据过期(stale)后,在合适时机(组件重新挂载、窗口重新聚焦、网络恢复)自动后台刷新。
  • 后台更新:用户始终看到的是缓存数据(即时展示),同时 Query 在后台进行新数据的静默替换,界面无闪烁。
  • 乐观更新:修改数据的操作先假设成功,立即更新缓存中的 UI,再等待服务端确认,若失败则回滚。体验流畅。
  • 分页/无限滚动:内置 useQuery 支持分页参数变化自动重取;useInfiniteQuery 提供“加载更多”的无限滚动能力。
  • 请求去重 / 重试 / 防抖:并发多个相同查询会自动去重为一个请求;请求失败可配置自动重试次数和间隔。
  • 并行与依赖查询:多个查询可以并行执行;查询可以依赖其他查询的结果后再触发。
  • 窗口焦点刷新:用户切回浏览器标签页时,自动刷新所有过期的查询,保证数据新鲜。
  • Devtools:官方浏览器扩展,可视化管理查询缓存、状态与手动触发刷新,调试利器。

14.2.2 快速上手:QueryClient 与 queryOptions

首先安装:

npm install @tanstack/react-query

在应用根节点设置 QueryClientProvider 并创建 QueryClient

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5分钟内视为新鲜数据,不会自动重取
      retry: 2,                  // 失败后重试2次
      refetchOnWindowFocus: false, // 可关闭窗口聚焦自动刷新
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

QueryClient 是全局缓存管理器,可以自定义默认行为。它的 staleTime 决定了数据多久变“陈旧”,期间不会触发自动重取,对静态资源或低频变化的数据非常有用。


14.2.3 数据查询:useQuery

useQuery 是最常用的 Hook,用于获取并缓存数据。它需要至少两个参数:

  • queryKey:唯一标识这个查询的键,通常是数组,例如 ['todos']['todo', id]。Query 以此对缓存做精确匹配。
  • queryFn:返回 Promise 的函数,就是实际请求数据的函数。
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

function Todos() {
  const fetchTodos = async () => {
    const { data } = await axios.get('/api/todos');
    return data;
  };

  const { data, isLoading, isError, error } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  });

  if (isLoading) return <div>加载中...</div>;
  if (isError) return <div>出错了:{error.message}</div>;

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

返回对象中包含丰富的状态字段:

  • data:请求成功后的数据。
  • isLoading:首次加载且无缓存时为 true。
  • isFetching:是否正在获取数据(包括后台刷新),可以用来显示全局加载指示器。
  • isError / error:是否出错及错误对象。
  • refetch:手动触发重新请求的函数。

依赖查询(enable 选项)

有时一个查询需要依赖另一个查询的结果,比如先获取用户 ID,再请求用户详情。使用 enabled 属性控制查询是否自动执行:

const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  enabled: !!userId, // userId 为 null 时禁用此查询
});

并行查询

多个查询可以并存,Query 自动并行发出请求:

const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
const { data: users } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });

自动后台刷新与 staleTime

默认 staleTime 为 0,意味着数据立即变成陈旧,每次挂载组件或聚焦窗口都会重新请求。生产实践中建议根据数据更新频率设置合理的 staleTime

  • 实时数据(如股票):0 或较短(如 1 秒)。
  • 用户个人资料:可能 5~30 分钟。
  • 配置字典:可能 24 小时甚至 Infinity(仅在手动刷新时更新)。

14.2.4 数据修改:useMutation

useMutation 用于执行创建、更新、删除等副作用操作。它返回一个 mutate 函数和相关状态。

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

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

  const addMutation = useMutation({
    mutationFn: (newTodo) => axios.post('/api/todos', newTodo),
    onSuccess: () => {
      // 添加成功后,让 todos 列表的缓存失效,触发后台刷新
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  return (
    <button
      onClick={() => {
        addMutation.mutate({ title: '新待办', completed: false });
      }}
    >
      {addMutation.isPending ? '正在添加...' : '添加待办'}
    </button>
  );
}

乐观更新

乐观更新可以瞬间提升用户体验,即使网络慢也觉得很快。实现方式是在 useMutationonMutate 中手动修改缓存,并在 onError 中回滚。

const updateMutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (updatedTodo) => {
    // 取消正在进行中的查询,避免它们覆盖我们的乐观更新
    await queryClient.cancelQueries({ queryKey: ['todos'] });

    // 快照当前缓存数据
    const previousTodos = queryClient.getQueryData(['todos']);

    // 乐观地更新缓存
    queryClient.setQueryData(['todos'], (old) =>
      old.map(todo => (todo.id === updatedTodo.id ? { ...todo, ...updatedTodo } : todo))
    );

    // 返回快照,用于错误回滚
    return { previousTodos };
  },
  onError: (err, updatedTodo, context) => {
    // 回滚到之前的状态
    queryClient.setQueryData(['todos'], context.previousTodos);
  },
  onSettled: () => {
    // 无论成功失败,最后都重新获取确保数据一致
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

cancelQueries 确保没有过时的请求覆盖我们的乐观更新。快照回滚保证了数据正确性。


14.2.5 分页与无限滚动

分页(Pagination)

分页查询只需将 page 参数放入 queryKey,Query 在 key 变化时自动重新获取:

function PaginatedTodos() {
  const [page, setPage] = useState(1);

  const { data, isLoading } = useQuery({
    queryKey: ['todos', page],
    queryFn: () => fetchTodos(page), // page 变化自动重新请求
    keepPreviousData: true, // 切换页时保留上一页数据,避免闪烁
  });

  // ... 渲染与分页控件
}

keepPreviousData 让翻页期间仍显示旧数据,直到新数据加载完成,体验更平滑。

无限滚动(useInfiniteQuery)

适合列表不断下滑加载更多的场景,如社交动态。

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

function InfiniteTodos() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['todos'],
    queryFn: ({ pageParam = 1 }) => fetchTodos(pageParam),
    getNextPageParam: (lastPage, allPages) => {
      // 根据最后一页数据判断是否有下一页
      return lastPage.hasMore ? allPages.length + 1 : undefined;
    },
  });

  return (
    <div>
      {data?.pages.map((page, i) => (
        <React.Fragment key={i}>
          {page.items.map(todo => <p key={todo.id}>{todo.title}</p>)}
        </React.Fragment>
      ))}
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage ? '加载中...' : '加载更多'}
      </button>
    </div>
  );
}

pages 是一个数组,每一项代表每次请求返回的一页数据。fetchNextPage 调用时会自动带上 pageParam


14.2.6 QueryClient 高级操作

QueryClient 不仅可以通过 useQueryClient 在组件内获取,也可以在组件外(如拦截器中)直接使用:

const queryClient = new QueryClient();

// 可以在请求拦截器里根据错误做全局处理
axios.interceptors.response.use(
  response => response,
  error => {
    if (error.response.status === 401) {
      queryClient.clear(); // 清空所有缓存
    }
    return Promise.reject(error);
  }
);

常用方法:

  • invalidateQueries:使某个查询缓存失效,下次组件挂载或触发重取时重新请求。
  • setQueryData:直接写入缓存(乐观更新)。
  • getQueryData:读取缓存数据。
  • refetchQueries:强制重新获取某些查询。
  • resetQueries:重置查询到初始状态。
  • clear:清空所有缓存。

14.2.7 实际项目中的典型配置

在大型应用中,通常会将 TanStack Query 与 Axios 一起封装,统一处理错误提示和 token 注入,并设定合理的全局 staleTimeretry 策略。例如:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5分钟
      retry: 1,
      refetchOnWindowFocus: false,
      // 全局错误处理,例如统一弹出错误提示
      onError: (error) => {
        message.error(`请求失败:${error.message}`);
      },
    },
    mutations: {
      onError: (error) => {
        message.error(`操作失败:${error.message}`);
      },
    },
  },
});

同时,将 API 请求方法抽离成 hooks,让组件只关心业务数据:

// hooks/useTodos.js
export function useTodos() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: () => todoApi.getList(),
  });
}

这使代码结构清晰,便于复用和测试。


TanStack Query 已经成为 React 生态中处理服务端状态的事实标准,大幅减少了手写 loading / error / 缓存逻辑的工作量。在掌握基本 API 后,建议深入理解 staleTimecacheTime 的区别、乐观更新的正确姿势以及 QueryClient 的全局配置,这些是生产环境调优的关键。