TanStack Query 技术指南:异步状态管理核心实践

在前端开发中,我们长期以来习惯于将后端API返回的数据,一股脑存入Redux、Zustand等全局状态管理库中。但很少意识到,后端数据所具备的异步性,决定了这种服务器状态(Server State)的管理,天生就伴随着诸多复杂难题------缓存失效难以控制、重复请求造成资源浪费、加载/错误状态的繁琐处理,这些问题往往需要编写大量冗余代码,还容易出现边缘漏洞。

TanStack Query 的出现,恰好为这些痛点提供了标准化、高效率的解决方案。它的核心价值,在于将异步数据请求、缓存管理等逻辑,从React声明式UI的生命周期中彻底解耦,无需手动编写复杂的缓存、重试、同步逻辑,极大降低了前端维护服务器状态同步的成本。本篇文章将由浅入深,带你完整掌握从环境搭建、基础使用,到高级缓存策略、实战技巧的全链路实践,轻松搞定异步状态管理

一、 环境集成

在现代前端项目中,推荐使用 pnpmbun 进行安装。

bash 复制代码
# 使用 npm
npm i @tanstack/react-query

# 使用 pnpm (推荐)
pnpm add @tanstack/react-query

# 使用 Yarn
yarn add @tanstack/react-query

# 使用 Bun
bun add @tanstack/react-query

二、 核心机制:查询 (useQuery)

useQuery 主要用于处理 幂等性 (同一个操作,执行一次和执行多次,最终结果完全一样,不会产生副作用)的读取操作。其核心在于通过 queryKey 实现对数据的自动化追踪。

1. 核心配置参数

  • queryKey : 必须为数组类型 unknown[]。它是缓存条目的唯一哈希索引。当数组内的依赖项(如 ID、分页参数)发生变化时,查询会自动重新触发。
tsx 复制代码
queryKey: ['todos', 1, 10, 'active', 'apple'] //todo是一条哈希索引,后面的参数是里面的值
  • queryFn: 必须返回一个 Promise。该函数负责执行实际的数据抓取逻辑。
  • enabled: 条件触发,控制是否执行查询(用于依赖请求)。
  • staleTime: 数据新鲜时间,期间不发请求,直接读缓存。
  • gcTime: 缓存保留时长,超时无引用则自动回收。
  • retry: 请求失败自动重试次数。
  • select: 缓存层数据转换,优化性能与组件渲染。
  • refetchOnWindowFocus: 切屏回退时是否自动刷新。
  • refetchOnMount: 组件挂载时是否自动重请求。

2. 状态监听与处理

useQuery 返回的对象包含状态,用于驱动 UI 交互:

  • status : 包括 pendingerrorsuccess
  • isFetching: 布尔值,表示当前是否正处于网络请求中。
tsx 复制代码
const { status, data, error } = useQuery({
  queryKey: ['todos', { filter: 'active' }],
  queryFn: fetchTodoList,
  staleTime: 5000,
});

if (status === 'pending') return <div>Loading...</div>;
if (status === 'error') return <div>Error: {error.message}</div>;

三、 数据变更:变更 (useMutation)

useMutation 用于处理非幂等(执行一次和多次的结果不同)的 增、删、改 操作。

1. 关键生命周期钩子

useQuery 不同,useMutation 提供了完整的请求生命周期回调,便于处理副作用:

  • onMutate: 请求发起前的同步回调,常用于"乐观更新"。
  • onSuccess: 请求成功后的操作。
  • onError: 异常捕获。
  • onSettled: 请求结束(无论成败)的清理工作。

2. 实践示例:

tsx 复制代码
const { mutate, isPending } = useMutation({
  mutationFn: (newTodo) => axios.post('/api/todos', newTodo),
  onSuccess: (data) => {
    // 逻辑处理
  },
  retry: false // 提交类操作通常建议关闭自动重试
});

四、 核心策略:查询失效与同步 (Invalidation)

在执行 Mutation 后,前端缓存往往与后端数据库不再同步。通过 queryClient.invalidateQueries,我们可以声明式地通知特定的缓存条目失效。

1. 原理说明

当查询被标记为"失效(Stale)"后:

  1. 该查询对应的 staleTime 会立即失效。
  2. 如果该查询当前正在页面上渲染,它会自动在后台触发重新拉取(Refetch)。

2. 代码实现

TypeScript

tsx 复制代码
const queryClient = useQueryClient();

const mutation = useMutation({
  mutationFn: updateApi,
  onSuccess: () => {
    // 自动刷新所有以 'userList' 开头的查询
    queryClient.invalidateQueries({
      queryKey: ['userList']
    });
  }
});

五、 进阶要点

1.全局最佳实践配置:

tsx 复制代码
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 30, // 30秒内不重新请求
      gcTime: 1000 * 60 * 5, // 缓存5分钟
      retry: 1, // 失败重试1次
      refetchOnWindowFocus: false, // 关闭切屏刷新
      refetchOnMount: true,
    },
  },
});

queryclient常用的几个方法:

  • getQueryData:同步获取缓存数据
  • setQueryData:同步修改缓存数据
  • invalidateQueries:标记数据过期并重新请求
  • cancelQueries:取消进行中的请求
  • prefetchQuery:预加载数据并存入缓存

2. 全局配置注入

在使用 Hooks 之前,必须在应用顶层注入 QueryClientProvider

tsx 复制代码
const queryClient = new QueryClient();

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

3. 依赖查询 (Dependent Queries)

当一个请求依赖于另一个请求的结果时,应使用 enabled 选项。

tsx 复制代码
const { data: user } = useQuery({ queryKey: ['user'], queryFn: fetchUser });

const { data: projects } = useQuery({
  queryKey: ['projects', user?.id],
  queryFn: () => fetchProjects(user.id),
  enabled: !!user?.id, // 仅在 user.id 存在时执行
});

4. 数据转换 (Select)

利用 select 配置项可以在缓存层面进行数据转换,避免在组件内部进行昂贵的计算。

tsx 复制代码
useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  select: (users) => users.map(user => user.name), // 仅返回名称列表
});

5.乐观更新:

tanstack的乐观更新是指在服务器响应前,前端立即更新本地的缓存与uI,假设操作会成功。若服务器报错则回滚。

流程:

  • 保存快照(副本):通过queryclient.getQueryData保存旧数据,作为服务器响应失败的回滚
  • 更新:在useMutation的onMutate钩子中,用queryclient.setQueryData直接修改缓存,ui无需响应就更新
  • 错误回滚:onError 中用快照恢复缓存;onSettled 无论成败都重新拉取,确保与服务端数据一致
tsx 复制代码
import { useMutation, useQueryClient } from '@tanstack/react-query';

const queryClient = useQueryClient();
const updateTodoMutation = useMutation({
  mutationFn: updateTodoApi, // 实际调用的 API
  onMutate: async (updatedTodo) => {
    await queryClient.cancelQueries({ queryKey: ['todos'] }); // 取消冲突请求
    const previousTodos = queryClient.getQueryData(['todos']); // 保存旧数据
    // 乐观更新缓存
    queryClient.setQueryData(['todos'], (old) =>
      old?.map(t => t.id === updatedTodo.id ? updatedTodo : t)
    );
    return { previousTodos }; // 返回回滚上下文
  },
  onError: (err, vars, context) => {
    // 失败时回滚
    queryClient.setQueryData(['todos'], context.previousTodos);
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] }); // 同步服务端最新
  }
});

6.分页 / 无限加载:

普通分页:

核心部分是:将页码放进query,这样将会自动缓存每一页。

tsx 复制代码
const fetchTodos = async ({ pageParam = 1 }) => {
  const res = await fetch(`/api/todos?page=${pageParam}&limit=10`);
  return res.json();
};

const { data, isLoading } = useQuery({
  queryKey: ['todos', page], // 页码变,就重新请求
  queryFn: () => fetchTodos(page),
});

无限加载:

核心:

  • pages:已经加载的所有页
  • fetchNextPage() :手动点的加载更多
  • hasNextPage:有没有下一页(true/false)
  • getNextPageParam你定义的规则, 下一页是几?
tsx 复制代码
import { useInfiniteQuery } from '@tanstack/react-query';

export default function Demo() {
  const fetchPages = async ({ pageParam = 1 }) => {
    const res = await fetch(`/api/list?page=${pageParam}&limit=2`);
    const data = await res.json();
    return data;
   // 后端返回格式:{ list: [数据], total: 总条数 }
  };

  // 2. 核心:无限加载
  const {
    pages,           // 已经加载的所有页(数组)
    fetchNextPage,   // 点击加载下一页
    hasNextPage,     // 有没有下一页
    getNextPageParam // 规则:下一页是第几页?
  } = useInfiniteQuery({
    queryKey: ['demo-list'], 
    queryFn: fetchPages,    
    
    getNextPageParam: (lastPage, allPages) => {
      // lastPage: 最后一页数据
      // allPages: 已经加载的所有页

      // 如果已经加载完所有,返回 undefined(没有下一页)
      if (allPages.length * 2 >= lastPage.total) {
        return undefined;
      }
      // 否则返回下一页页码
      return allPages.length + 1;
    }
  });

  return (
    <div>
      <h2>已加载的页:pages</h2>
      {pages?.map((page, index) => (
        <div key={index} style={{ border: '1px solid', margin: 10, padding: 10 }}>
          <h4>第 {index + 1} 页</h4>
          {page.list.map(item => (
            <div key={item.id}>{item.title}</div>
          ))}
        </div>
      ))}

      {/* 点击加载更多 */}
      <button 
        onClick={() => fetchNextPage()} 
        disabled={!hasNextPage}
      >
        {hasNextPage ? '加载下一页' : '已加载全部'}
      </button>
    </div>
  );
}

7.预加载:

tsx 复制代码
 const prefetchNextPage = async () => {
    await queryClient.prefetchQuery({
      // 要预加载的缓存 key(必须和 useQuery 里一样)
      queryKey: ['todos', 2],

      // 请求函数
      queryFn: () => fetch('/api/todos?page=2').then(res => res.json()),

      // 可选:多久内不再重复预加载(默认 30s)
      staleTime: 1000 * 60,
    });

六、 资源引用

七、 实战结合:

TanStack Query 的设计初衷,从来不是取代 Redux、Zustand 等经典状态管理方案,而是填补前端架构中服务器状态管理 的空白。两者各司其职、深度融合,才是现代 React 项目的最优架构 ------ 让数据逻辑更纯粹,让状态管理更轻盈,真正实现锦上添花的效果。

相关推荐
种花家的强总2 小时前
前端项目开发/维护中降低成本的方式之一:降低耦合度
前端
Palpitate_LL2 小时前
从XSS到“RCE“的PC端利用链构建
前端·xss
qq_334466862 小时前
Edge 浏览器不要提示还原页面
前端·edge
孟祥_成都2 小时前
复刻字节 AI 开发流:实践 Node.js 通用脚手架
前端·人工智能·node.js
xiaotao1312 小时前
第十二章:TypeScript 深度集成
前端·vite·前端打包
前端Hardy2 小时前
前端开发效率翻倍:15个超级实用的工具函数,直接复制进项目(建议收藏)
前端·javascript·面试
前端Hardy2 小时前
Vue 项目必备:10 个高频实用自定义指令,直接复制即用(Vue2 / Vue3 通用)
前端·javascript·vue.js
CHU7290352 小时前
知识触手可及:在线教学课堂APP的沉浸式学习体验
前端·学习·小程序
患得患失9492 小时前
【css技巧】用 CSS 实现:移入立即执行,移出延时返回
前端·css