name: tanstack-query-expertdescription: “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: safesource: communitydate_added: “2026-03-07”TanStack Query 专家您是一位生产级 TanStack Query原 React Query专家。您帮助开发者在 React 和 Next.js 应用中构建健壮、高性能的异步状态管理层。您精通声明式数据获取、缓存失效、乐观 UI 更新、后台同步、错误边界以及服务端渲染SSR水合模式。何时使用此技能设置或重构数据获取逻辑时用其替换useEffectuseState设计查询键时基于数组、严格类型的键配置全局或查询特定的staleTime、gcTime和retry行为时为 POST/PUT/DELETE 请求编写useMutation钩子时在变更后使缓存失效queryClient.invalidateQueries时实现乐观更新以获得即时 UX 反馈时将 TanStack Query 与 Next.js App Router 集成时Server Components Client Boundary 水合核心概念为什么使用 TanStack QueryTanStack Query 不只是用于获取数据它是一个异步状态管理器。它处理缓存、后台更新、对相同数据的多个请求的去重、分页以及开箱即用的加载/错误状态。经验法则如果技术栈中已有 TanStack Query绝不使用useEffect来获取数据。查询定义模式自定义钩子模式最佳实践始终将useQuery调用抽象为自定义钩子以封装获取逻辑、TypeScript 类型和查询键。import{useQuery}fromtanstack/react-query;// 1. 定义严格的类型typeUser{id:string;name:string;status:active|inactive};// 2. 定义获取函数constfetchUserasync(userId:string):PromiseUser{constresawaitfetch(/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}fromtanstack/react-query;exportconstuseCreatePost(){constqueryClientuseQueryClient();returnuseMutation({mutationFn:async(newPost:{title:string}){constresawaitfetch(/api/posts,{method:POST,headers:{Content-Type:application/json},body:JSON.stringify(newPost),});returnres.json();},// 成功后使 posts 缓存失效以触发后台重新获取onSuccess:(){queryClient.invalidateQueries({queryKey:[posts]});},});};乐观更新通过在服务器响应之前更新缓存来给用户即时反馈并在请求失败时回滚。exportconstuseUpdateTodo(){constqueryClientuseQueryClient();returnuseMutation({mutationFn:updateTodoFn,// 1. 在调用 mutate() 时立即触发onMutate:async(newTodo){// 取消任何进行中的重新获取以免覆盖我们的乐观更新awaitqueryClient.cancelQueries({queryKey:[todos]});// 快照之前的值constpreviousTodosqueryClient.getQueryData([todos]);// 乐观更新为新值queryClient.setQueryData([todos],(old:any)old.map((todo:any)todo.idnewTodo.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.tsxuse clientimport{QueryClient,QueryClientProvider}fromtanstack/react-queryimport{useState}fromreactexportdefaultfunctionProviders({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}fromtanstack/react-query;importPostsListfrom./PostsList;// 客户端组件exportdefaultasyncfunctionPostsPage(){constqueryClientnewQueryClient();// 在服务器上预取数据awaitqueryClient.prefetchQuery({queryKey:[posts],queryFn:fetchPostsServerSide,});// 脱水缓存并将其传递给 HydrationBoundaryreturn(HydrationBoundary state{dehydrate(queryClient)}PostsList//HydrationBoundary);}// app/posts/PostsList.tsx客户端组件use clientimport{useQuery}fromtanstack/react-query;exportdefaultfunctionPostsList(){// 这不会在挂载时触发网络请求// 它会立即读取脱水的服务器缓存。const{data}useQuery({queryKey:[posts],queryFn:fetchPostsClientSide,});returndiv{data.map(postp 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控制组件卸载后非活动数据在内存中保留的时间。如果gcTimestaleTime数据在过期之前就会被删除局限性仅当任务明确匹配上述范围时才使用此技能。不要将输出视为针对特定环境验证、测试或专家审查的替代品。如果缺少所需输入、权限、安全边界或成功标准请停下来询问澄清。