“你是否还在为这些数据问题熬夜?”
- 反复编写
useEffect处理加载/错误状态,代码冗余率高达60% - 页面跳转后返回,数据明明已更新却显示陈旧内容
- 同一接口被多个组件重复调用,性能急剧下降
- 全局状态库中混杂着服务器数据与UI状态,维护如走钢丝
👉 这正是React Query要解决的革命性问题
传统数据获取 vs React Query
传统方式:手动管理的地狱
// 痛点代码:16行状态管理 + 易错的依赖项
function usePhotos() {
const [loading, setLoading] = useState(false);
const [photos, setPhotos] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
photoAPI.getAll(1).
then(
data => setPhotos(data)
).catch(
e => setError(e)).finally(() => setLoading(false)
);
}, []);
// ❗ 缺少刷新依赖项可能导致数据过期
return { loading, photos, error };
}
⚠️ 隐藏问题:
- 无自动缓存策略,后退页面时数据重置
- 多组件同时调用时产生重复请求
- 需要手动实现轮询/懒加载等高级功能
- 可能会不小心拿到“过时”的数据
- 数据缓存
- 性能优化,如分页和延迟加载数据
React Query 方案:声明式优雅
// 解决方案:1行核心代码获得完整能力
function usePhotos() {
return useQuery({
queryKey: ['photos'],
queryFn: () => photoAPI.getAll(1),
staleTime: 5 * 60 * 1000
// ⚡自动5分钟缓存
});
}
🚀 自动获得:
- 智能缓存(内存 & 持久化可选)
- 重复请求自动合并(Deduplication)
- 后台静默刷新(Stale-While-Revalidate)
- 滚动恢复时的即时数据展示
React Query Example
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import { getTodos, postTodo } from '../my-api'
// Create a client
const queryClient = new QueryClient()
function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
// Access the client
const queryClient = useQueryClient()
// Queries
const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })
// Mutations
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<div>
<ul>{query.data?.map((todo) => <li key={todo.id}>{todo.title}</li>)}</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
render(<App />, document.getElementById('root'))
核心概念
query的核心概念就是下面三个:queries、mutations、query invalidation,让我来向你一一介绍:
Queries(查询)
Queries是对与唯一键绑定的异步数据源的声明性依赖项。Queries可以与任何基于 Promise 的方法(包括 GET 和 POST 方法)一起使用,以从服务器获取数据。如果您的方法修改了服务器上的数据,我们建议改用 Mutations。
要在组件或自定义 hook 中订阅查询,使用以下命令调用 useQuery hook:查询的唯一键(在内部用于在整个应用程序中重新获取、缓存和共享查询)&&一个函数(返回一个 Promise,该 Promise:解析数据或引发错误)。
import { useQuery } from '@tanstack/react-query'
function App() {
const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
}
result 对象包含一些非常重要的状态,query在任何给定时刻只能处于以下状态之一:
isPending或status === 'pending'- 查询尚无数据isError或status === 'error'- 查询遇到错误isSuccess或status === 'success'- 查询成功且数据可用
除了这些主要状态之外,根据查询的状态,还可以获得更多信息:
error- 如果查询处于isError状态,则可通过 error 属性获取错误。data- 如果查询处于isSuccess状态,则可通过 data 属性获取数据。isFetching- 在任何状态下,如果查询在任何时间(包括后台重新获取)都在提取,则 isFetching 将为 true。
对于大多数查询,通常只需检查 isPending 状态,然后检查 isError 状态,最后假设数据可用并呈现成功状态:
function Todos() {
const { isPending, isError, data, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
if (isPending) {
return <span>Loading...</span>
}
if (isError) {
return <span>Error: {error.message}</span>
}
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
查询键: 查询键必须是顶层的 Array,可以像具有单个字符串的 Array 一样简单,也可以像包含许多字符串和嵌套对象的 Array 一样复杂。只要查询键是可序列化的,并且对于查询数据是唯一的,您就可以使用它!
Mutations(突变)
Mutations通常用于创建/更新/删除数据或执行服务器副作用。为此,TanStack Query 导出了一个 useMutation 钩子。
下面是一个向服务器添加新 todo 的 mutation 示例:
function App() {
const mutation = useMutation({
mutationFn: (newTodo) => {
return axios.post('/todos', newTodo)
},
})
return (
<div>
{mutation.isPending ? (
'Adding todo...'
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>Todo added!</div> : null}
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: 'Do Laundry' })
}}
>
Create Todo
</button>
</>
)}
</div>
)
}
在任何给定时刻,更改只能处于以下状态之一:
isIdle或status === 'idle'- 变更当前处于空闲状态或处于全新/重置状态isPending或status === 'pending'- 更改当前正在运行isError或status === 'error'- 更改遇到错误isSuccess或status === 'success'- 更改成功且更改数据可用
除了这些主要状态之外,根据更改的状态,还可以获得更多信息:
error- 如果更改处于 error 状态,则可通过 error 属性获取错误。data- 如果更改处于成功状态,则可通过 data 属性获取数据。
在上面的示例中,您还看到,您可以通过使用单个变量或对象调用 mutate 函数来将变量传递给 mutations 函数。
重要提示:mutate 函数是一个异步函数,这意味着你不能直接在 React 16 及更早版本的事件回调中使用它。如果需要访问 onSubmit 中的事件,则需要将 mutate 包装在另一个函数中。
// This will not work in React 16 and earlier
const CreateTodo = () => {
const mutation = useMutation({
mutationFn: (event) => {
event.preventDefault()
return fetch('/api', new FormData(event.target))
},
})
return <form onSubmit={mutation.mutate}>...</form>
}
// This will work
const CreateTodo = () => {
const mutation = useMutation({
mutationFn: (formData) => {
return fetch('/api', formData)
},
})
const onSubmit = (event) => {
event.preventDefault()
mutation.mutate(new FormData(event.target))
}
return <form onSubmit={onSubmit}>...</form>
}
Query Invalidation(查询失效)
等待查询变得过时后再再次获取它们并不总是有效的,尤其是当您知道查询的数据由于用户执行的某些作而过时时。为此,QueryClient 有一个 invalidateQueries 方法,它允许您智能地将查询标记为过时,并可能重新获取它们!
// Invalidate every query in the cache
queryClient.invalidateQueries()
// Invalidate every query with a key that starts with `todos`
queryClient.invalidateQueries({ queryKey: ['todos'] })
当查询因 invalidateQueries 而失效时,会发生两种情况:
- 它被标记为过时。这种过时的状态会覆盖 useQuery 或相关钩子中使用的任何 staleTime 配置
- 如果查询当前正在通过 useQuery 或相关钩子渲染,它也将在后台重新获取
使用 invalidateQueries 进行查询匹配
当使用 invalidateQueries 和 removeQueries(以及其他支持部分查询匹配的 API)时,您可以按前缀匹配多个查询,或者获得非常具体并匹配确切的查询。
在此示例中,我们可以使用 todos 前缀使查询键中以 todos 开头的任何查询无效:
import { useQuery, useQueryClient } from '@tanstack/react-query'
// Get QueryClient from the context
const queryClient = useQueryClient()
queryClient.invalidateQueries({ queryKey: ['todos'] })
// Both queries below will be invalidated
const todoListQuery = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
})
const todoListQuery = useQuery({
queryKey: ['todos', { page: 1 }],
queryFn: fetchTodoList,
})
Query 运行流程
React Query 的生命周期始于组件挂载或依赖项变化时,首先会检查缓存中是否存在与当前查询键(Query Key)匹配且未过期的数据。如果存在有效缓存,则立即将缓存数据返回给组件使用,此时组件会直接进入 isSuccess 状态,用户界面得以快速呈现,无需等待网络请求。这一缓存优先策略是 React Query 高性能的核心设计之一,它避免了不必要的网络请求,显著提升了用户体验。
如果缓存不存在或已过期,React Query 会发起一个新的网络请求。在请求发出后,查询状态会立即标记为 isPending,此时组件可以展示加载状态以提供用户反馈。请求发出后,系统会进入等待响应阶段,这个过程可能成功也可能失败。如果请求成功,返回的数据会被存入缓存,并将查询状态更新为 isSuccess,触发组件重新渲染以展示最新数据。如果请求失败,错误信息会被记录,状态标记为 isError,组件可以据此展示错误提示,同时 React Query 会根据配置的 retry 策略自动尝试重新请求。
当数据成功获取并进入缓存后,便开始受到 staleTime 配置的控制。staleTime 决定了数据在缓存中的保鲜期,默认值为 0 表示获取后立即视为过期。一旦数据过期,查询会被标记为 stale 状态,但这是一个内部状态,不会立即影响用户体验。此时如果相关组件仍在渲染,React Query 会在后台静默发起新的请求来更新数据,这就是所谓的 Stale-While-Revalidate 策略。这种设计使得用户总能立即看到数据(即便是过期的),同时在后台悄无声息地保持数据新鲜度。
对于不再渲染的组件,其查询数据会继续保留在缓存中,直到垃圾回收机制根据 gcTime 的设置将其清除。gcTime 默认为 5 分钟,控制着未使用数据在内存中的保留时间。这个机制有效平衡了内存使用和数据复用需求,避免了内存无限制增长。
除了自动更新外,React Query 还提供了强大的手动干预能力。开发者可以主动调用 invalidateQueries 来标记特定查询为过期,强制触发后台更新。也可以直接使用 setQueryData 修改本地缓存,这在实现乐观更新时特别有用。这些手动操作与自动机制完美配合,为开发者提供了精细的控制能力,同时又不失框架的自动化优势。
Query 的替代性
TanStack Query 会取代 Redux 或其他全局状态管理器吗?
场景
Redux适用场景
- 跨组件共享的UI状态(如主题、侧边栏开关)
- 需要时间旅行调试的复杂交互流程
- 客户端计算的派生状态(如购物车总价)
React Query适用场景
- 所有异步服务端状态(GET/POST请求)
- 需要缓存策略的数据(如用户信息、配置项)
- 高频更新的实时数据(股票行情、聊天消息)
// Redux管理客户端状态
const theme = useSelector(state => state.ui.theme);
// React Query管理服务端状态
const { data: user } = useQuery(['user'], fetchUser);
性能对比表格
| 特性 | 传统方式 | Redux Thunk | React Query |
|---|---|---|---|
| 自动缓存 | ❌ | ❌ | ✅ |
| 重复请求合并 | ❌ | ❌ | ✅ |
| 后台自动刷新 | ❌ | ❌ | ✅ |
| 内存占用 | 低 | 高 | 极低 |
| 代码量(相同功能) | 100% | 150% | 30% |
实战场景:电商后台管理
场景1:商品列表页
- React Query自动缓存分页数据
- 切换页面时保留滚动位置,瞬时展示缓存
- 后台每5分钟静默刷新库存信息
场景2:订单提交
- 使用
useMutation处理下单请求 - 乐观更新显示成功状态,无需等待接口返回
- 提交后自动刷新用户余额查询
结语
React Query不是要取代Redux,而是将开发者从繁琐的异步逻辑中解放。当你的应用有超过30%的状态来自服务端时,它就是你的下一个必备武器。
🌟 现在就开始:用10%的代码量,获得200%的性能提升!