@tanstack/react-query:React 服务器状态管理与数据同步解决方案
1 概述
@tanstack/react-query 是一个功能强大的异步状态管理库,专为简化 React 应用中的服务器状态管理而设计。它提供了一套全面的工具集,用于处理数据获取、缓存、同步和更新服务器状态,使开发者能够专注于业务逻辑而非数据获取细节。
作为现代前端开发的关键工具,@tanstack/react-query 解决了传统数据获取方案中的诸多痛点:
- 自动缓存与失效管理,减少不必要的网络请求
- 智能重试与背景刷新机制,提升用户体验
- 简洁的 API 设计,降低异步数据处理的复杂性
- 深度整合 TypeScript,提供完善的类型安全保障
- 与 React 生态无缝集成,支持 Suspense 等现代特性
最新版本 v5.84.2 带来了多项重要改进,包括统一的参数对象化 API、增强的类型推断能力、优化的缓存策略以及对 React 18 特性的更好支持。这些更新进一步提升了库的易用性和性能,使其成为构建高性能 React 应用的理想选择。
无论是小型项目还是大型企业级应用,@tanstack/react-query 都能显著简化数据管理流程,减少样板代码,并提供一致且可预测的状态管理体验。
2 安装与配置
2.1 环境要求
@tanstack/react-query v5 需要以下环境支持:
- React: 18.0.0 或更高版本
- TypeScript: 4.7 或更高版本(可选)
- Node.js: 16.0.0 或更高版本
2.2 安装命令
使用 npm
# 安装核心库
npm install @tanstack/react-query# 安装开发工具(可选,用于调试)
npm install --save-dev @tanstack/react-query-devtools# 如果使用 TypeScript,确保安装类型定义
npm install --save-dev @types/react @types/react-dom
使用 yarn
# 安装核心库
yarn add @tanstack/react-query# 安装开发工具
yarn add -D @tanstack/react-query-devtools
使用 pnpm
# 安装核心库
pnpm add @tanstack/react-query# 安装开发工具
pnpm add -D @tanstack/react-query-devtools
安装指定版本
如需安装特定版本(如本文介绍的 v5.84.2):
npm install @tanstack/react-query@5.84.2
npm install --save-dev @tanstack/react-query-devtools@5.84.2
2.3 项目初始化
创建新项目(可选)
如果您正在创建新的 React 项目,推荐使用以下方式:
# 使用 Create React App
npx create-react-app my-app --template typescript
cd my-app# 或使用 Vite(推荐,更快的开发体验)
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install# 安装 React Query
npm install @tanstack/react-query @tanstack/react-query-devtools
2.4 基本配置
步骤 1:创建 QueryClient
在 src/lib/queryClient.ts
文件中创建 QueryClient 实例:
// src/lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query'export const queryClient = new QueryClient({defaultOptions: {queries: {staleTime: 60 * 1000, // 数据保鲜时间:1分钟gcTime: 10 * 60 * 1000, // 垃圾回收时间:10分钟retry: 1, // 失败重试次数refetchOnWindowFocus: true, // 窗口聚焦时重新获取refetchOnReconnect: true, // 网络重连时重新获取},mutations: {retry: 1, // 变更操作重试次数},},
})
步骤 2:配置应用入口
在 src/main.tsx
(Vite)或 src/index.tsx
(CRA)中配置 Provider:
// src/main.tsx (Vite) 或 src/index.tsx (CRA)
import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { queryClient } from './lib/queryClient'
import App from './App'
import './index.css'ReactDOM.createRoot(document.getElementById('root')!).render(<React.StrictMode><QueryClientProvider client={queryClient}><App />{/* 仅在开发环境显示开发工具 */}{process.env.NODE_ENV === 'development' && (<ReactQueryDevtools initialIsOpen={false} />)}</QueryClientProvider></React.StrictMode>
)
步骤 3:配置详细选项
如需更精细的控制,可以扩展 QueryClient 配置:
// src/lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query'export const queryClient = new QueryClient({defaultOptions: {queries: {// 数据缓存配置staleTime: 5 * 60 * 1000, // 5分钟内数据被认为是新鲜的gcTime: 30 * 60 * 1000, // 30分钟后清理未使用的数据// 重试配置retry: (failureCount, error) => {// 对于 404 错误不重试if (error.status === 404) return false// 最多重试 3 次return failureCount < 3},retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),// 自动重新获取配置refetchOnWindowFocus: true,refetchOnReconnect: true,refetchOnMount: true,// 网络模式networkMode: 'online', // 'online' | 'always' | 'offlineFirst'},mutations: {retry: 1,networkMode: 'online',},},
})
2.5 第一个查询示例
创建 API 函数
首先创建一个 API 服务文件 src/services/api.ts
:
// src/services/api.ts
export interface User {id: numbername: stringemail: stringusername: string
}export interface Post {id: numbertitle: stringbody: stringuserId: number
}// 模拟 API 调用
const API_BASE = 'https://jsonplaceholder.typicode.com'export const fetchUsers = async (): Promise<User[]> => {const response = await fetch(`${API_BASE}/users`)if (!response.ok) {throw new Error('获取用户列表失败')}return response.json()
}export const fetchUser = async (userId: number): Promise<User> => {const response = await fetch(`${API_BASE}/users/${userId}`)if (!response.ok) {throw new Error(`获取用户 ${userId} 失败`)}return response.json()
}export const fetchPosts = async (): Promise<Post[]> => {const response = await fetch(`${API_BASE}/posts`)if (!response.ok) {throw new Error('获取文章列表失败')}return response.json()
}
创建第一个组件
创建 src/components/UserList.tsx
组件:
// src/components/UserList.tsx
import { useQuery } from '@tanstack/react-query'
import { fetchUsers, User } from '../services/api'export function UserList() {const {data: users,isPending,isError,error} = useQuery({queryKey: ['users'],queryFn: fetchUsers,staleTime: 5 * 60 * 1000, // 5分钟内数据保持新鲜})if (isPending) {return <div className="loading">正在加载用户列表...</div>}if (isError) {return (<div className="error"><h3>加载失败</h3><p>{error.message}</p><button onClick={() => window.location.reload()}>重试</button></div>)}return (<div className="user-list"><h2>用户列表</h2><div className="users">{users?.map((user: User) => (<div key={user.id} className="user-card"><h3>{user.name}</h3><p>用户名: {user.username}</p><p>邮箱: {user.email}</p></div>))}</div></div>)
}
在 App 组件中使用
更新 src/App.tsx
:
// src/App.tsx
import { UserList } from './components/UserList'
import './App.css'function App() {return (<div className="App"><header className="App-header"><h1>React Query 示例应用</h1></header><main><UserList /></main></div>)
}export default App
添加基本样式
在 src/App.css
中添加样式:
/* src/App.css */
.App {max-width: 1200px;margin: 0 auto;padding: 20px;font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}.App-header {text-align: center;margin-bottom: 30px;
}.loading, .error {text-align: center;padding: 20px;margin: 20px 0;
}.error {background-color: #fee;border: 1px solid #fcc;border-radius: 4px;color: #c33;
}.error button {background-color: #c33;color: white;border: none;padding: 8px 16px;border-radius: 4px;cursor: pointer;margin-top: 10px;
}.user-list h2 {margin-bottom: 20px;
}.users {display: grid;grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));gap: 20px;
}.user-card {background: #f9f9f9;padding: 20px;border-radius: 8px;border: 1px solid #e0e0e0;
}.user-card h3 {margin: 0 0 10px 0;color: #333;
}.user-card p {margin: 5px 0;color: #666;
}
运行应用
现在您可以启动应用查看效果:
npm run dev # 如果使用 Vite
# 或
npm start # 如果使用 Create React App
打开浏览器访问 http://localhost:5173
(Vite)或 http://localhost:3000
(CRA),您将看到:
- 加载状态: 初始显示"正在加载用户列表…"
- 数据展示: 加载完成后显示用户卡片列表
- 错误处理: 如果网络失败,显示错误信息和重试按钮
- 开发工具: 按 F12 可以看到 React Query DevTools(如果已安装)
关键概念说明
这个示例展示了 React Query 的核心概念:
- queryKey:
['users']
- 唯一标识这个查询 - queryFn:
fetchUsers
- 实际的数据获取函数 - 自动缓存: 相同的查询键会复用缓存数据
- 加载状态:
isPending
表示首次加载 - 错误处理:
isError
和error
提供错误信息 - 类型安全: TypeScript 自动推断
users
的类型为User[] | undefined
3 核心API变更
3.1 参数对象化
v5 版本统一采用对象参数格式,替代了之前的多重载形式:
// 旧版用法
useQuery('todos', fetchTodos, { staleTime: 5000 })// v5 新用法
useQuery({queryKey: ['todos'],queryFn: fetchTodos,staleTime: 5000
})
这一变更提高了类型安全性和代码一致性,所有核心钩子(useQuery、useInfiniteQuery、useMutation 等)均采用相同模式。
3.2 状态命名调整
状态命名更加精确,明确区分不同加载阶段:
// 旧版
{ isLoading, isSuccess, isError }// v5 新版
{ isPending, isLoading, isSuccess, isError }
isPending
: 查询已开始但尚未有数据isLoading
: 首次加载数据(初始加载)isFetching
: 数据刷新中(背景刷新)
3 TypeScript 支持
3.1 类型推断优化
v5 大幅改进了类型推断能力,大多数情况下无需显式指定泛型:
// 自动推断 data 类型为 Group[] | undefined
const fetchGroups = () : Promise<Group[]> => axios.get('/groups').then((response) => response.data)const { data } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups
})
3.2 类型窄化
通过状态判断自动窄化数据类型:
const { data, isSuccess } = useQuery({ queryKey: ['user'], queryFn: fetchUser
})if (isSuccess) {// TypeScript 知道此时 data 一定存在console.log(data.name) // 正确推断类型
}
3.3 全局错误类型配置
可通过模块增强定义全局错误类型:
import '@tanstack/react-query'
import type { AxiosError } from 'axios'declare module '@tanstack/react-query' {interface Register {defaultError: AxiosError}
}// 错误类型自动推断为 AxiosError | null
const { error } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups })
4 核心功能示例
4.1 基础查询
const { data, isPending, isError, error
} = useQuery({queryKey: ['todos', todoId], // 数组形式的查询键queryFn: () => fetchTodo(todoId),staleTime: 60 * 1000, // 数据保鲜时间 1 分钟retry: 2 // 失败重试次数
})if (isPending) return <div>加载中...</div>
if (isError) return <div>错误: {error.message}</div>return <div>{data.title}</div>
4.2 数据转换
使用 select
选项转换查询结果:
const { data } = useQuery({queryKey: ['user'],queryFn: fetchUser,select: (user) => ({id: user.id,fullName: `${user.firstName} ${user.lastName}`})
})// data 类型自动推断为 { id: number; fullName: string }
4.3 变更操作
使用 useMutation
处理数据更新:
const queryClient = useQueryClient()const mutation = useMutation({mutationFn: updateTodo,onSuccess: () => {// 更新成功后使相关查询失效,触发重新获取queryClient.invalidateQueries({ queryKey: ['todos'] })}
})mutation.mutate({ id: 1, title: '更新后的标题' })if (mutation.isPending) return <button disabled>保存中...</button>return (<button onClick={() => mutation.mutate({ id: 1, title: '新标题' })}>更新</button>
)
4.4 查询选项复用
使用 queryOptions
helper 复用查询配置:
import { queryOptions } from '@tanstack/react-query'function todoOptions(todoId: number) {return queryOptions({queryKey: ['todos', todoId],queryFn: () => fetchTodo(todoId),staleTime: 5 * 1000})
}// 在组件中使用
const { data } = useQuery(todoOptions(1))// 在预获取中使用
queryClient.prefetchQuery(todoOptions(2))
4 高级查询功能
4.1 无限查询
使用 useInfiniteQuery 实现无限滚动或加载更多:
import { useInfiniteQuery } from '@tanstack/react-query'function fetchProjects({ pageParam = 0 }) {return fetch(`/api/projects?page=${pageParam}`).then(res => res.json())
}function Projects() {const {data,fetchNextPage,hasNextPage,isFetchingNextPage,} = useInfiniteQuery({queryKey: ['projects'],queryFn: fetchProjects,getNextPageParam: (lastPage) => lastPage.nextCursor,initialPageParam: 0,})return (<div>{data?.pages.map((page, index) => (<div key={index}>{page.projects.map(project => (<p key={project.id}>{project.name}</p>))}</div>))}<buttononClick={() => fetchNextPage()}disabled={!hasNextPage || isFetchingNextPage}>{isFetchingNextPage ? '加载中...' : '加载更多'}</button></div>)
}
4.2 依赖查询
实现查询之间的依赖关系:
function UserProfile({ userId }) {const { data: user } = useQuery({queryKey: ['user', userId],queryFn: () => fetchUser(userId),})// 依赖 user 数据的查询const { data: posts } = useQuery({queryKey: ['posts', user?.id],queryFn: () => fetchPosts(user.id),enabled: !!user, // 仅当 user 存在时执行})return <div>{/* 渲染用户和帖子 */}</div>
}
5 状态更新与缓存
5.1 乐观更新
在服务器确认前更新UI,提升用户体验:
const queryClient = useQueryClient()const mutation = useMutation({mutationFn: updateTodo,onMutate: async (newTodo) => {// 取消当前查询await queryClient.cancelQueries({ queryKey: ['todo', newTodo.id] })// 保存当前数据const previousTodo = queryClient.getQueryData(['todo', newTodo.id])// 乐观更新queryClient.setQueryData(['todo', newTodo.id], newTodo)// 返回上下文return { previousTodo }},onError: (err, newTodo, context) => {// 发生错误时回滚queryClient.setQueryData(['todo', newTodo.id],context.previousTodo)},onSettled: (newTodo) => {// 无论成功失败,重新验证queryClient.invalidateQueries({ queryKey: ['todo', newTodo.id] })},
})
5.2 查询失效与重新验证
手动控制查询缓存:
// 使单个查询失效
queryClient.invalidateQueries({ queryKey: ['todos', 1] })// 使所有 todos 相关查询失效
queryClient.invalidateQueries({ queryKey: ['todos'] })// 强制重新获取,忽略缓存
queryClient.invalidateQueries({ queryKey: ['todos'], force: true })// 预获取数据
queryClient.prefetchQuery({queryKey: ['todos', 2],queryFn: () => fetchTodo(2),
})
6 性能优化
6.1 查询结果选择器
使用 select 优化重渲染:
const { data: todoCount } = useQuery({queryKey: ['todos'],queryFn: fetchTodos,select: (todos) => todos.filter(todo => !todo.completed).length,
})
6.2 禁用自动重试
在表单提交等场景禁用自动重试:
useMutation({mutationFn: submitForm,retry: false, // 禁用重试
})
6.3 结构化查询键
使用数组形式的查询键提高缓存效率:
// 基础查询键
useQuery({ queryKey: ['todos'] })// 带参数的查询键
useQuery({ queryKey: ['todos', { status: 'active', page: 1 }] })// 嵌套资源查询键
useQuery({ queryKey: ['user', 1, 'posts'] })
7 最佳实践
7.1 自定义 Hooks 封装
将查询逻辑封装为自定义 Hooks:
// hooks/useTodos.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'export function useTodos() {return useQuery({queryKey: ['todos'],queryFn: fetchTodos,})
}export function useAddTodo() {const queryClient = useQueryClient()return useMutation({mutationFn: addTodo,onSuccess: () => {queryClient.invalidateQueries({ queryKey: ['todos'] })},})
}
7.2 错误处理策略
全局错误处理配置:
// 创建客户端时配置全局错误处理
const queryClient = new QueryClient({defaultOptions: {queries: {onError: (error) => {console.error('查询错误:', error)// 可以在这里显示全局错误提示},},mutations: {onError: (error) => {console.error('变更错误:', error)},},},
})
7.3 与 React Suspense 集成
使用 useSuspenseQuery 简化加载状态处理:
import { useSuspenseQuery } from '@tanstack/react-query'function TodoDetails({ todoId }) {const { data } = useSuspenseQuery({queryKey: ['todo', todoId],queryFn: () => fetchTodo(todoId),})return <div>{data.title}</div>
}// 在父组件中使用 Suspense
function TodoPage({ todoId }) {return (<React.Suspense fallback={<div>加载中...</div>}><TodoDetails todoId={todoId} /></React.Suspense>)
}
8 常见问题与故障排除
8.1 安装相关问题
问题:安装后导入失败
Error: Cannot find module '@tanstack/react-query'
解决方案:
- 确认是否正确安装:
npm list @tanstack/react-query
- 清理缓存并重新安装:
npm cache clean --force
rm -rf node_modules package-lock.json
npm install
- 检查 Node.js 版本是否符合要求(>= 16.0.0)
问题:TypeScript 类型错误
Cannot find module '@tanstack/react-query' or its corresponding type declarations
解决方案:
确保安装了正确的版本和类型定义:
npm install @tanstack/react-query@latest
npm install --save-dev @types/react @types/react-dom
8.2 配置相关问题
问题:Provider 未正确配置
Error: useQuery must be used within a QueryClientProvider
解决方案:
确保在应用根部正确包装 QueryClientProvider
:
// ❌ 错误
function App() {return <UserList /> // useQuery 在这里会失败
}// ✅ 正确
function App() {return (<QueryClientProvider client={queryClient}><UserList /></QueryClientProvider>)
}
问题:开发工具不显示
解决方案:
- 确认已安装开发工具:
npm install --save-dev @tanstack/react-query-devtools
- 正确导入和使用:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'// 确保在 QueryClientProvider 内部
<QueryClientProvider client={queryClient}><App /><ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
8.3 查询相关问题
问题:数据不会自动刷新
可能原因:
staleTime
设置过长- 禁用了自动重新获取
解决方案:
useQuery({queryKey: ['data'],queryFn: fetchData,staleTime: 0, // 立即标记为过期refetchOnWindowFocus: true, // 窗口聚焦时刷新refetchOnReconnect: true, // 网络重连时刷新
})
问题:查询一直处于 pending 状态
可能原因:
queryFn
没有返回 Promise- 网络请求被阻塞
enabled
选项设置为 false
解决方案:
- 检查
queryFn
是否正确:
// ❌ 错误 - 没有返回 Promise
const fetchData = () => {fetch('/api/data').then(res => res.json())
}// ✅ 正确
const fetchData = () => {return fetch('/api/data').then(res => res.json())
}// ✅ 或使用 async/await
const fetchData = async () => {const res = await fetch('/api/data')return res.json()
}
- 检查
enabled
选项:
useQuery({queryKey: ['data'],queryFn: fetchData,enabled: true, // 确保查询已启用
})
问题:错误处理不生效
解决方案:
确保在 queryFn
中正确抛出错误:
const fetchData = async () => {const response = await fetch('/api/data')// ❌ 错误 - 不会触发错误状态if (!response.ok) {console.error('请求失败')return null}// ✅ 正确 - 会触发错误状态if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`)}return response.json()
}
8.4 性能相关问题
问题:组件频繁重渲染
可能原因:
- 查询键包含不稳定的引用
- 没有使用
select
优化数据选择
解决方案:
- 使用稳定的查询键:
// ❌ 错误 - 每次渲染都会创建新对象
const { data } = useQuery({queryKey: ['user', { id: userId, active: true }],queryFn: fetchUser
})// ✅ 正确 - 使用基本类型
const { data } = useQuery({queryKey: ['user', userId, 'active'],queryFn: fetchUser
})
- 使用
select
优化:
const { data } = useQuery({queryKey: ['users'],queryFn: fetchUsers,select: users => users.filter(user => user.active), // 只在数据变化时重新计算
})
问题:内存占用过高
解决方案:
调整缓存配置:
const queryClient = new QueryClient({defaultOptions: {queries: {gcTime: 5 * 60 * 1000, // 5分钟后清理未使用的数据staleTime: 1 * 60 * 1000, // 1分钟后标记为过期},},
})
8.5 网络相关问题
问题:离线时查询失败
解决方案:
配置网络模式:
useQuery({queryKey: ['data'],queryFn: fetchData,networkMode: 'offlineFirst', // 离线优先模式retry: (failureCount, error) => {// 网络错误时不重试if (error.name === 'NetworkError') return falsereturn failureCount < 3}
})
问题:请求超时
解决方案:
在 queryFn
中添加超时控制:
const fetchWithTimeout = async (url: string, timeout = 10000) => {const controller = new AbortController()const timeoutId = setTimeout(() => controller.abort(), timeout)try {const response = await fetch(url, {signal: controller.signal})clearTimeout(timeoutId)if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`)}return response.json()} catch (error) {clearTimeout(timeoutId)throw error}
}
8.6 调试技巧
启用详细日志
import { QueryClient } from '@tanstack/react-query'const queryClient = new QueryClient({logger: {log: console.log,warn: console.warn,error: console.error,},
})
使用浏览器开发工具
- React Query DevTools: 查看查询状态、缓存数据和网络请求
- Network 面板: 监控实际的网络请求
- Console 面板: 查看错误信息和调试日志
- React DevTools: 检查组件状态和 props
添加调试信息
const { data, error, isError } = useQuery({queryKey: ['debug-query'],queryFn: async () => {console.log('查询开始执行')const result = await fetchData()console.log('查询结果:', result)return result},onError: (error) => {console.error('查询失败:', error)},onSuccess: (data) => {console.log('查询成功:', data)}
})
通过以上指南,您应该能够解决大部分在使用 @tanstack/react-query 过程中遇到的问题。如果问题仍然存在,建议查看官方文档或在 GitHub 仓库中提交 issue。