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,数据在过期之前就会被删除!
局限性
- 仅当任务明确匹配上述范围时才使用此技能。
- 不要将输出视为针对特定环境验证、测试或专家审查的替代品。
- 如果缺少所需输入、权限、安全边界或成功标准,请停下来询问澄清。