本篇依然来自于我们的 《前端周刊》 项目!
由团队成员 0bipinnata0 翻译,这位佬有技术追求、翻译风格精准细腻,还擅长挖掘原文背后的技术细节~
欢迎大家 进群 针对React Query 用法同该佬深度交流😁 以及持续追踪全球最新前端资讯!!
原文:What is React Query? The Missing Data-Fetching Tool for React Apps

正文如下:
React Query在短短一年内增长了50%,现在已被超过20%的React应用使用。是什么推动了开发者如此迅速地采用它?答案在于React Query如何解决React中数据获取的根本挑战。
大多数React应用依赖于useEffect和useState钩子的组合来管理服务器状态。这种方法最初有效,但很快就会创建一个错综复杂的逻辑网络,变得越来越难以维护。React Query提供了一条不同的路径,通过提供一种声明式方式来获取、缓存和同步React应用中的数据。
数据说明了一切。只有2.8%的用户报告不喜欢React Query,而超过44%的开发者现在更喜欢它而非其他管理服务器状态的替代方案。这些不仅仅是虚荣指标------它们反映了一个真正解决开发者日常面临的实际问题的工具。
本指南将带您了解为什么React Query对现代React开发变得非常重要。我们将探索React Query的用途,以及它如何通过提供开箱即用的缓存、同步和服务器状态管理来简化API交互。您将看到实用示例并学习使React Query成为许多开发者一直在寻找的缺失部分的基础知识。
让我们来看看为什么传统的数据获取方法不足,以及React Query如何彻底改变游戏规则。
为什么React需要更好的数据获取工具
传统的React应用中的数据获取依赖于useState和useEffect钩子。这种方法最初看起来很简单,但随着应用变得更加复杂,基本限制开始显现,使这种模式越来越难以管理。
useEffect和useState对异步数据的限制
React团队官方建议在大多数情况下不要使用useEffect进行数据获取。这一建议源于几个影响应用性能和开发者体验的重要缺点。
在useEffect中直接使用异步函数会立即产生问题。这个钩子期望返回的要么是空,要么是清理函数------而不是一个promise。尝试将useEffect回调标记为异步会导致意外行为,可能会扰乱React的渲染生命周期。
管理加载和错误状态需要多个必须仔细同步的状态变量:
React
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
随着数据源数量的增加,这种模式变得冗长且容易出错。当组件在异步操作完成前卸载时,经常会发生内存泄漏。没有适当的清理,在已卸载的组件上设置状态会导致运行时错误。
Effect不在服务器上运行,这使服务器端渲染变得复杂。开发者必须为服务器和客户端渲染实现不同的数据获取策略,增加了开发过程的复杂性。
常见陷阱:竞态条件和重复请求 竞态条件是使用useEffect获取数据时最有问题的问题之一。当多个异步操作以意外顺序完成时,这些情况会发生,可能会显示过时或不正确的信息。
考虑一个用户快速输入的搜索功能。如果第一个搜索查询比后续查询花费更长时间解析,UI可能会显示较早查询的结果,而不是最近的查询。正如一位开发者所描述的:"你搜索Macron,然后改变主意搜索Trump,最终你得到的是你想要的(Trump)和你得到的(Macron)之间的不匹配"。
对于某些应用,这些竞态条件有严重后果,例如"用户购买错误产品,或医生给患者开错药"。
重复请求是另一个常见问题。浏览器通常限制到同一主机的并发连接数------例如,Chrome只允许6个并行请求。当组件同时触发多个请求时,这种限制会创建一个瓶颈,其中额外的请求必须等待,显著影响性能。
React 18的严格模式加剧了这些问题。在开发过程中,组件会挂载、卸载和重新挂载,可能会触发相同的数据获取效果两次。
为什么管理服务器状态与客户端状态不同 客户端状态和服务器状态有根本不同的特性,需要不同的管理方法。客户端状态包括表单输入和UI切换,这些是您的应用直接控制的。服务器状态由来自API的远程存在并需要同步的数据组成。
服务器状态在几个关键方面与客户端状态不同:
-
外部实体可能在您的应用不知情的情况下更新它
-
它需要显式的获取和更新操作
-
它涉及与网络相关的问题,如加载状态、错误处理和缓存 传统的状态管理工具如Redux或Context API主要是为客户端状态设计的。它们缺乏处理服务器状态特定挑战的内置功能。
随着应用规模的扩大,使用useState
和useEffect
获取服务器数据的组合变得越来越复杂。开发者必须为每个数据源手动实现加载状态、错误处理、缓存、后台更新和过时数据管理。
这种复杂性解释了为什么像React Query这样的专门工具变得流行。它们提供了一种声明式方法来管理服务器状态,内置解决上述挑战的方案。React Query不是替代客户端状态管理,而是通过专注于服务器状态需求来补充它。
React Query使开发者能够编写更易于维护的代码,同时避免与传统useState
/useEffect
方法相关的陷阱。这种关注点分离对于构建健壮、可扩展的React应用至关重要。
什么是React Query,它解决了什么问题?
TanStack Query(前身为React Query)代表了React应用中管理服务器状态的专门解决方案。它的核心功能是作为异步状态管理器,而不仅仅是数据获取库。这种区别很重要,因为它塑造了我们思考和与远程数据交互的方式。
使用useQuery()进行声明式数据获取
useQuery钩子构成了React Query的基础,使开发者能够以声明式而非命令式方式获取数据。这个钩子封装了整个数据获取过程,而不是手动编排状态变量和副作用:
React
const { data, isLoading, error } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
这种模式抽象了复杂性,同时提供了对加载状态、错误处理和获取数据的即时访问。queryKey作为查询的唯一标识符,而queryFn包含检索数据的返回promise的函数。
这种方法的强大之处在于它如何将异步代码从命令式转变为声明式。这种转变消除了通常使用useState/useEffect模式所需的大约50%的样板代码。
内置缓存和后台更新
React Query的复杂缓存机制解决了数据管理最具挑战性的方面之一。该库根据查询键自动缓存查询结果,使对同一端点的后续调用几乎即时。
以下是生命周期的工作原理:
-
当组件首次使用特定键调用useQuery时,React Query获取数据并缓存它
-
如果另一个组件请求相同的数据,它会立即接收缓存的结果,同时在后台进行刷新
-
在使用查询的所有组件卸载后,缓存会在默认期限(5分钟)内持续存在,然后进行垃圾回收
React Query通过配置选项提供精细控制:
-
staleTime
:确定数据何时需要刷新(默认:立即) -
gcTime
:控制未使用的数据在内存中保留多长时间(默认:5分钟) -
refetchOnMount
、refetchOnWindowFocus
、refetchOnReconnect
:在适当时智能刷新数据 这些功能以最少的开发者干预自动化客户端和服务器之间的数据同步。
React Query如何简化异步状态管理
服务器状态由于其异步性质和远程持久性而面临独特挑战------传统状态管理工具不是为有效处理这些特性而设计的。
React Query通过几个关键机制解决了这种复杂性:
首先,它通过基于查询键而非组件实例存储状态来消除竞态条件。这防止了过时数据覆盖较新响应的情况,这是基于useEffect方法的常见问题。
其次,它提供自动后台获取指示器。虽然status === 'loading'
表示初始加载,但isFetching
布尔值表示后台刷新,允许更细微的加载状态:
React
{isFetching ? <div>刷新中...</div> : null}
第三,React Query有效地去重多个相同的请求,即使在React的StrictMode中也能防止冗余网络调用。这种优化节省了带宽并提高了性能,特别是在较大的应用中。
最后,该库擅长通过类型级别的区分联合管理异步状态转换,确保加载、错误和成功状态保持清晰分离。
React Query将缓存转变为服务器状态的事实上的数据存储,消除了仅为处理异步数据而需要复杂状态管理架构的需求。这种范式转变使开发者能够专注于业务逻辑,而不是数据同步的复杂机制。
在项目中设置React Query
设置React Query只需几分钟,但对于如何管理服务器状态的影响是立竿见影的。您需要安装包,创建QueryClient实例,并用提供者包装您的应用。让我们逐步了解每个步骤。
安装@tanstack/react-query
React Query已更名为TanStack Query,因此您将以其当前名称安装包。选择您喜欢的包管理器:
Shell
npm install @tanstack/react-query
其他包管理器同样适用:
Shell
pnpm add @tanstack/react-query
yarn add @tanstack/react-query
bun add @tanstack/react-query
这个库适用于React v18+,并支持ReactDOM和React Native。如果您遇到提及react-query
的旧教程,请记住,从v4开始,该包现在可作为@tanstack/react-query
使用。
创建和配置QueryClient
QueryClient管理您应用的服务器状态。它作为所有查询的中心枢纽,并提供与缓存交互的方法。
创建基本QueryClient实例:
React
import { QueryClient } from '@tanstack/react-query';
const queryClient = new QueryClient();
要更好地控制行为,传递配置选项:
React
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
// 其他查询默认值
},
},
});
关键配置参数包括:
-
staleTime
:数据在需要重新获取前保持新鲜的时间 -
gcTime
:非活动数据在缓存中保留的时间(默认:5分钟) -
refetchOnMount
、refetchOnWindowFocus
:自动重新获取行为 QueryClient包含我们将在后面部分探讨的预取、缓存和使查询无效的方法。
在应用中使用QueryClientProvider
创建后,使用QueryClientProvider组件使您的QueryClient在整个应用中可用。这个提供者必须包装使用React Query钩子的任何组件。
将此添加到应用的根组件:
React
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* 您的应用组件 */}
</QueryClientProvider>
);
}
QueryClientProvider接受client作为prop,它必须是QueryClient的实例。这创建了一个React上下文,为所有嵌套组件提供QueryClient。
在典型的React应用中,将此提供者放在组件树的根部附近:
React
function App() {
return (
<QueryClientProvider client={queryClient}>
<Router>
<ThemeProvider theme={theme}>
<Routes />
</ThemeProvider>
</Router>
</QueryClientProvider>
)
}
现在,此提供者树中的任何组件都可以使用React Query钩子,如useQuery
和useMutation
。
作为可选但推荐的步骤,安装ESLint插件以捕获潜在错误:
Shell
npm install -D @tanstack/eslint-plugin-query
这个插件识别React Query实现中的常见错误和不一致。
在项目中设置React Query后,您已准备好开始更高效地获取数据。下一节将向您展示useQuery
钩子实际应用的实例。
使用useQuery()获取数据:实用指南
useQuery钩子代表了React Query功能的基石。这个单一钩子替代了开发者在使用传统方法时通常需要处理的复杂状态管理代码。
基本useQuery()语法和参数
两个基本参数驱动useQuery钩子:
React
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
});
queryKey
作为缓存和跟踪查询的唯一标识符。虽然简单的字符串有效,但即使对于基本查询,也建议使用数组------它为以后添加参数提供了灵活性。React Query使用此键来确定何时重新获取数据和管理缓存失效。
queryFn
参数包含您的获取数据的异步函数。此函数必须返回一个解析为您的数据或抛出错误的promise:
React
const fetchPosts = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
return response.json();
};
像enabled
、staleTime
和refetchInterval
这样的额外配置选项允许您根据特定需求自定义获取行为。
处理加载、错误和成功状态
React Query擅长管理异步操作的各种状态。钩子返回一个包含几个基本属性的对象:
React
const {
data, // 获取的数据
isLoading, // 初始加载期间为true
isError, // 如果发生错误则为true
error, // 如果存在则为错误对象
status, // 'loading'、'error'或'success'
isFetching // 在任何获取期间为true(包括后台)
} = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
});
这些属性使渲染不同的UI状态变得非常简单:
React
if (isLoading) return <div>加载中...</div>;
if (isError) return <div>错误:{error.message}</div>;
// 成功状态(数据可用)
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
对于更明确的状态处理,您可以使用status
属性:
React
switch (status) {
case 'loading':
return <div>获取帖子中...</div>;
case 'error':
return <div>发生错误:{error.message}</div>;
case 'success':
return <DisplayPosts data={data} />;
}
isFetching
属性对于显示后台刷新指示器而不中断主UI特别有价值。
使用JSONPlaceholder API的React Query示例
这里是使用JSONPlaceholder API获取帖子列表的完整示例:
React
import { useQuery } from '@tanstack/react-query';
function Posts() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
return response.json();
}
});
if (isLoading) return <p>加载中...</p>;
if (error) return <p>出错了:{error.message}</p>;
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
基于ID获取单个帖子需要更专门的方案:
React
function SinglePost({ postId }) {
const { data, status } = useQuery({
queryKey: ['post', postId],
queryFn: () => fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
.then(res => res.json()),
enabled: !!postId // 只在postId存在时运行查询
});
if (status === 'loading') return <p>加载帖子中...</p>;
if (status === 'error') return <p>加载帖子出错</p>;
return (
<div>
<h1>{data.title}</h1>
<p>{data.body}</p>
</div>
);
}
enabled
选项展示了基于依赖关系的条件数据获取------这是实际应用中的常见需求。
使用useMutation()修改数据
获取数据只是故事的一半。应用还需要创建、更新和删除服务器数据。useMutation
钩子以与React Query对数据获取同样优雅的方式处理这些操作。
useMutation()如何用于POST、PUT、DELETE
useMutation
钩子管理修改服务器上数据的操作------主要是POST、PUT、PATCH和DELETE请求。与自动运行的useQuery
不同,只有在您明确触发时,变更才会执行。
以下是基本设置:
React
import { useMutation } from '@tanstack/react-query';
const mutation = useMutation({
mutationFn: (newTodo) => {
return axios.post('/todos', newTodo);
}
});
mutationFn
参数接受任何返回promise的函数。这种灵活性涵盖了不同的HTTP方法,用于各种数据修改需求:
React
// POST请求创建资源
const createTodo = useMutation({
mutationFn: (newTodo) => axios.post('/todos', newTodo)
});
// PUT请求更新资源
const updateTodo = useMutation({
mutationFn: (todo) => axios.put(`/todos/${todo.id}`, todo)
});
// DELETE请求删除资源
const deleteTodo = useMutation({
mutationFn: (id) => axios.delete(`/todos/${id}`)
});
触发变更和处理响应
定义后,通过调用带有变量的mutate函数触发变更:
React
<button onClick={() => mutation.mutate({ title: '洗衣服' })}>
创建待办事项
</button>
变更钩子提供几个状态来跟踪进度:
React
{mutation.isPending && <p>添加待办事项中...</p>}
{mutation.isError && <p>错误:{mutation.error.message}</p>}
{mutation.isSuccess && <p>待办事项已添加!</p>}
React Query还提供mutateAsync
,它返回一个promise,启用await语法:
React
const handleSubmit = async () => {
try {
const data = await mutation.mutateAsync(newTodo);
console.log('成功:', data);
} catch (error) {
console.error('错误:', error);
}
};
对于更受控的执行,直接将回调选项传递给mutate函数:
React
mutation.mutate(
{ id: 5, name: '洗衣服' },
{
onSuccess: (data) => {
console.log('成功:', data);
},
onError: (error) => {
console.error('错误:', error);
}
}
);
变更后使查询无效
这是变更真正闪光的地方。成功变更后,您通常希望刷新受影响的数据。React Query使用invalidateQueries
方法使这变得简单:
React
import { useMutation, useQueryClient } from '@tanstack/react-query';
function TodoApp() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: addTodo,
onSuccess: () => {
// 使待办事项列表无效并重新获取
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
}
使无效将查询标记为过时,促使React Query立即重新获取活动查询,同时仅在再次使用其他查询时重新获取它们。
或者,您可以使用setQueryData
直接更新缓存,适用于变更响应包含更新数据的情况:
React
const mutation = useMutation({
mutationFn: editTodo,
onSuccess: (data, variables) => {
queryClient.setQueryData(['todo', { id: variables.id }], data);
},
});
这种方法通过使用变更响应直接更新缓存来避免不必要的网络请求,同时仍然保持不变性:
React
// ✅ 正确的不可变更新
queryClient.setQueryData(['posts', { id }], (oldData) =>
oldData ? { ...oldData, title: '新标题' } : oldData
);
useQuery
用于数据获取和useMutation
用于数据修改一起提供了在React应用中管理服务器状态的完整解决方案。
实际应用的高级功能
当您超越基本数据获取时,React Query的真正优势才会显现。实际应用需要传统方法无法高效提供的复杂功能。
使用keepPreviousData进行分页
任何构建过分页界面的人都知道这种挫折感:用户点击"下一页",突然看到他们数据所在位置的加载旋转器。这很刺耳,感觉像是坏了。
React Query通过keepPreviousData选项解决了这个问题。您的应用在获取下一页的同时继续显示上一页的数据,而不是在页面转换之间显示加载状态。用户体验变得无缝------没有闪烁的内容。
React Query还提供isPreviousData标志来帮助您优雅地处理这个问题。当显示以前的数据时,您可以禁用导航按钮,防止用户点击页面的速度快于数据加载的速度。
使用useInfiniteQuery()进行无限滚动
构建无限滚动界面传统上需要复杂的状态管理来跟踪多页数据。useInfiniteQuery钩子完全消除了这种复杂性。
与常规查询不同,它返回专为无限滚动设计的结构化响应:
-
data.pages数组包含所有获取的页面
-
fetchNextPage函数加载更多数据
-
hasNextPage布尔值确定是否存在更多数据
-
isFetchingNextPage区分初始和后续加载
实现变得如此简单:
React
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
getNextPageParam: (lastPage) => lastPage.nextPage
});
getNextPageParam
函数告诉React Query下一页请求使用什么参数。React Query自动处理其他所有内容。
使用enabled标志的依赖查询
许多应用需要链式查询------首先获取用户数据,然后获取该用户的项目。传统方法导致复杂的useEffect链,难以调试。
React Query使用enabled
选项优雅地处理这个问题:
React
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: getUserByEmail
});
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: getProjectsByUser,
enabled: !!user?.id
});
项目查询等待用户数据可用。不需要复杂的依赖数组或效果链。
后台重新获取和staleTime
React Query通过智能后台重新获取自动保持数据新鲜。当用户返回您的标签页、网络重新连接或组件重新挂载时,它会触发更新。
staleTime
选项让您控制这种行为。设置它来确定数据在后台更新开始前保持新鲜的时间。较长的过时时间减少不必要的网络请求,而较短的时间确保数据保持最新。
这种自动同步意味着您的用户总是看到最新信息,而您不需要任何额外代码。
结论
React Query已证明自己是React应用中管理服务器状态的基本工具。它始于解决useState和useEffect的限制,现已成为超过44%处理服务器数据的开发者的首选。
传统的数据获取方法创造了不必要的复杂性。竞态条件、内存泄漏和冗长的状态管理代码困扰着仅依赖React内置钩子的应用。React Query通过其直观的useQuery和useMutation钩子以及自动工作的复杂缓存系统直接解决了这些问题。
设置非常简单。创建QueryClient,用QueryClientProvider包装您的应用,并立即开始使用钩子。这种最小配置解锁了强大的功能,否则需要广泛的自定义实现。
React Query在复杂应用中真正脱颖而出。keepPreviousData用于平滑分页、useInfiniteQuery用于无限滚动和带有enabled标志的条件查询等功能将高级数据模式转变为简单实现。自动后台重新获取使您的应用保持最新,无需手动干预。
变更通过相同的声明式方法处理数据修改,完成了这幅图景。缓存失效系统确保您的UI在每次更改后与服务器状态保持同步,消除了通常需要的手动协调。
对于构建与API交互的React应用的开发者来说,React Query已变得不可或缺。它从根本上改变了我们处理服务器状态管理的方式,使我们能够专注于功能而非数据同步机制。改进的开发者体验和应用性能使其成为任何处理服务器数据的React项目的有价值补充。