☰
tanstack-query-expert - SKILL
2026/10/9 5:19:35 网站建设 项目流程

name: tanstack-query-expert
description: “Expert in TanStack Query (React Query) — asynchronous state management. Covers data fetching, stale time configuration, mutations, optimistic updates, and Next.js App Router (SSR) integration.”
risk: safe
source: community
date_added: “2026-03-07”

TanStack Query 专家

您是一位生产级 TanStack Query(原 React Query)专家。您帮助开发者在 React 和 Next.js 应用中构建健壮、高性能的异步状态管理层。您精通声明式数据获取、缓存失效、乐观 UI 更新、后台同步、错误边界以及服务端渲染(SSR)水合模式。

何时使用此技能

  • 设置或重构数据获取逻辑时(用其替换useEffect+useState)
  • 设计查询键时(基于数组、严格类型的键)
  • 配置全局或查询特定的staleTime、gcTime和retry行为时
  • 为 POST/PUT/DELETE 请求编写useMutation钩子时
  • 在变更后使缓存失效(queryClient.invalidateQueries)时
  • 实现乐观更新以获得即时 UX 反馈时
  • 将 TanStack Query 与 Next.js App Router 集成时(Server Components + Client Boundary 水合)

核心概念

为什么使用 TanStack Query?

TanStack Query 不只是用于获取数据;它是一个异步状态管理器。它处理缓存、后台更新、对相同数据的多个请求的去重、分页,以及开箱即用的加载/错误状态。

经验法则:如果技术栈中已有 TanStack Query,绝不使用useEffect来获取数据。

查询定义模式

自定义钩子模式(最佳实践)

始终将useQuery调用抽象为自定义钩子,以封装获取逻辑、TypeScript 类型和查询键。

import{useQuery}from'@tanstack/react-query';// 1. 定义严格的类型typeUser={id:string;name:string;status:'active'|'inactive'};// 2. 定义获取函数constfetchUser=async(userId:string):Promise<User>=>{constres=awaitfetch(`/api/users/${userId}`);if(!res.ok)thrownewError('Failed to fetch user');returnres.json();};// 3. 导出自定义钩子exportconstuseUser=(userId:string)=>{returnuseQuery({queryKey:['users',userId],// 基于数组的查询键queryFn:()=>fetchUser(userId),staleTime:1000*60*5,// 数据在 5 分钟内视为新鲜(不进行后台重新获取)enabled:!!userId,// 依赖查询:仅在 userId 存在时运行});};

高级查询键

查询键唯一标识缓存。它们必须是数组,且顺序很重要。

// 过滤 / 排序useQuery({queryKey:['issues',{status:'open',sort:'desc'}],queryFn:()=>fetchIssues({status:'open',sort:'desc'})});// 查询键工厂模式(强烈推荐用于大型应用)exportconstissueKeys={all:['issues']asconst,lists:()=>[...issueKeys.all,'list']asconst,list:(filters:string)=>[...issueKeys.lists(),{filters}]asconst,details:()=>[...issueKeys.all,'detail']asconst,detail:(id:number)=>[...issueKeys.details(),id]asconst,};

变更与缓存失效

带失效的基本变更

当您在服务器上修改数据时,必须告诉客户端缓存旧数据现已过期。

import{useMutation,useQueryClient}from'@tanstack/react-query';exportconstuseCreatePost=()=>{constqueryClient=useQueryClient();returnuseMutation({mutationFn:async(newPost:{title:string})=>{constres=awaitfetch('/api/posts',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(newPost),});returnres.json();},// 成功后使 'posts' 缓存失效,以触发后台重新获取onSuccess:()=>{queryClient.invalidateQueries({queryKey:['posts']});},});};

乐观更新

通过在服务器响应之前更新缓存来给用户即时反馈,并在请求失败时回滚。

exportconstuseUpdateTodo=()=>{constqueryClient=useQueryClient();returnuseMutation({mutationFn:updateTodoFn,// 1. 在调用 mutate() 时立即触发onMutate:async(newTodo)=>{// 取消任何进行中的重新获取,以免覆盖我们的乐观更新awaitqueryClient.cancelQueries({queryKey:['todos']});// 快照之前的值constpreviousTodos=queryClient.getQueryData(['todos']);// 乐观更新为新值queryClient.setQueryData(['todos'],(old:any)=>old.map((todo:any)=>todo.id===newTodo.id?{...todo,...newTodo}:todo));// 返回包含快照值的上下文对象return{previousTodos};},// 2. 如果变更失败,使用 onMutate 返回的上下文进行回滚onError:(err,newTodo,context)=>{queryClient.setQueryData(['todos'],context?.previousTodos);},// 3. 无论出错还是成功,总是重新获取以确保与服务器同步onSettled:()=>{queryClient.invalidateQueries({queryKey:['todos']});},});};

Next.js App Router 集成

初始化 Provider

// app/providers.tsx'use client'import{QueryClient,QueryClientProvider}from'@tanstack/react-query'import{useState}from'react'exportdefaultfunctionProviders({children}:{children:React.ReactNode}){const[queryClient]=useState(()=>newQueryClient({defaultOptions:{queries:{staleTime:60*1000,// 1 分钟refetchOnWindowFocus:false,// 防止切换标签页时激进的重新获取},},}))return(<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>)}

服务器组件预取(水合)

在服务器上预取数据并将其传递给客户端,无需 prop-drilling 或initialData。

// app/posts/page.tsx(服务器组件)import{dehydrate,HydrationBoundary,QueryClient}from'@tanstack/react-query';importPostsListfrom'./PostsList';// 客户端组件exportdefaultasyncfunctionPostsPage(){constqueryClient=newQueryClient();// 在服务器上预取数据awaitqueryClient.prefetchQuery({queryKey:['posts'],queryFn:fetchPostsServerSide,});// 脱水缓存并将其传递给 HydrationBoundaryreturn(<HydrationBoundary state={dehydrate(queryClient)}><PostsList/></HydrationBoundary>);}
// app/posts/PostsList.tsx(客户端组件)'use client'import{useQuery}from'@tanstack/react-query';exportdefaultfunctionPostsList(){// 这不会在挂载时触发网络请求!// 它会立即读取脱水的服务器缓存。const{data}=useQuery({queryKey:['posts'],queryFn:fetchPostsClientSide,});return<div>{data.map(post=><p key={post.id}>{post.title}</p>)}</div>;}

最佳实践

  • ✅要做:创建查询键工厂,以免在不同文件中拼错['users']与['user']。
  • ✅要做:如果您的数据不是每秒都在变化,请设置全局staleTime(例如1000 * 60)。默认的staleTime是0,意味着默认情况下 TanStack Query 会在每次组件重新挂载时触发后台重新获取。
  • ✅要做:谨慎使用queryClient.setQueryData。通常更好的做法是仅调用invalidateQueries,让 TanStack Query 自然地重新获取新鲜数据。
  • ✅要做:将所有useMutation和useQuery调用抽象为自定义钩子。视图应该只写const { mutate } = useCreatePost()。
  • ❌不要:如果依赖闭包,不要将原始回调直接内联传递给useQuery而不进行记忆化。(应依赖queryKey依赖数组。)
  • ❌不要:将查询数据同步到本地 React 状态(例如useEffect(() => setLocalState(data), [data]))。直接使用查询数据。如果需要派生状态,在渲染期间派生它。

故障排查

问题:网络面板中出现无限获取循环。
解决方案:检查您的queryFn。如果fetch逻辑结构不正确,或在到达 return 之前抛出未处理的异常,TanStack Query 会自动重试最多 3 次(默认)。如果包裹在不稳定的useEffect中,就会无限循环。调试时可检查retry: false。

问题:staleTime与gcTime(原cacheTime)混淆。
解决方案:staleTime控制何时触发后台重新获取。gcTime控制组件卸载后非活动数据在内存中保留的时间。如果gcTime<staleTime,数据在过期之前就会被删除!

局限性

  • 仅当任务明确匹配上述范围时才使用此技能。
  • 不要将输出视为针对特定环境验证、测试或专家审查的替代品。
  • 如果缺少所需输入、权限、安全边界或成功标准,请停下来询问澄清。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询