如何使用React Query管理异步请求&数据
一、简介
react-query 是一个基于Hooks的React库,主要用于管理异步数据获取、缓存和状态更新。它通过提供一系列方法来处理数据获取、缓存、失效、重试以及其他数据管理方面的问题。使用声明式API轻松地处理数据获取和状态管理。React Query将数据获取和状态管理任务转移到后台,并提供了一个简单地API来处理 缓存、重试和错误处理
以前叫 React query,后面开始支持vue等其他前端框架,改名叫 TanStack Query(@tanstack/react-query)
二、功能
数据管理功能
-
数据获取
-
自动缓存:数据会被自动缓存,避免重复请求。
-
加载状态:提供
isLoading和isFetching状态,方便 UI 渲染。 -
错误处理:提供
isError和error状态,便于错误处理。 -
依赖查询:支持根据条件动态获取数据。
-
-
数据缓存
-
缓存过期时间:通过
staleTime和cacheTime控制缓存的有效期。 -
后台刷新:在数据过期时自动在后台刷新。
-
缓存共享:多个组件共享同一缓存,避免重复请求。
-
-
数据更新
-
乐观更新:在请求完成前更新 UI,提升用户体验。
-
自动失效:在数据更新后,自动使相关缓存失效并重新获取数据。
-
副作用处理:通过
onSuccess、onError等回调处理副作用。
-
-
数据同步
-
窗口聚焦刷新:在用户重新聚焦窗口时自动刷新数据。
-
网络重连刷新:在网络重新连接时自动刷新数据。
-
轮询:通过
refetchInterval定期刷新数据。
-
-
数据清理
-
窗口聚焦刷新:在用户重新聚焦窗口时自动刷新数据。
-
网络重连刷新:在网络重新连接时自动刷新数据。
-
轮询:通过
refetchInterval定期刷新数据。
-
-
数据状态管理
-
加载状态:
isLoading、isFetching。 -
错误状态:
isError、error。 -
数据状态:
data。 -
乐观更新状态:通过
setQueryData手动更新缓存。
-
数据展示和交互功能
-
分页和无限滚动支持
-
React Query支持分页和无限滚动的数据获取,适用于需要展示大量数据的场景。
-
-
与React Suspense集成
-
通过设置useQuery钩子的suspense选项为true,可以与React Suspense集成,实现更优雅的异步数据加载和错误处理。
-
插件和扩展功能
-
react-query/devtools
-
提供了一个React DevTools面板,用于查看和管理React Query的缓存、请求历史和查询状态。这有助于开发者在开发过程中更高效地调试和优化应用。
-
-
react-query/hydration
-
在服务器端渲染(SSR)应用程序中,可以自动提取数据并在客户端上进行缓存。这有助于减少客户端的加载时间,提升用户体验。
-
-
react-query/persistCache
-
可以将缓存存储在本地存储中,以便在刷新页面后重新加载数据。这有助于保持应用的状态一致性,提升用户体验。
-
三、使用
安装和使用
使用react-query,可以通过npm或yarn进行安装:
npm install react-query
# 或者
yarn add react-query
在React应用中,通常需要创建一个QueryClient实例,并通过QueryClientProvider将其传递给应用的根组件:
javascriptCopy Codeimport { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();
// 根组件
function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
// 其他组件使用时
const queryClient = useQuery({ queryKey: ['todos'], queryFn: getTodos })
这样,整个应用中的所有组件都可以通过useQuery和useMutation等Hooks来访问和管理数据。
useQueryClient()
它允许你在组件中获取当前的 QueryClient 实例。QueryClient 是 React Query 的核心,负责管理查询的缓存、状态以及触发查询等操作。通过 useQueryClient,你可以在组件中方便地访问这些功能。
-
QueryClient实例的方法-
fetchQuery:手动触发一个查询,并返回查询结果。如果缓存中有对应的数据且未过期,则可以直接使用缓存数据;否则,会重新请求数据并缓存。
-
invalidateQueries:标记查询为过期,下次使用时重新获取,是“懒惰”更新。
-
refetchQueries:立即强制重新获取查询数据,是“主动”更新。
-
getQueryData:从缓存中获取指定查询的数据。
-
setQueryData:手动设置指定查询的缓存数据。
-
resetQueries:将指定的查询重置为初始状态。如果查询具有
initialData,则数据将被重置为该数据。 -
removeQueries:从缓存中删除指定的查询。
-
cancelQueries:取消指定的查询。
-
-
注意事项
-
确保
QueryClientProvider包裹 -
避免频繁调用
-
理解查询状态和生命周期(
isLoading、isSuccess、isError等状态)
-
PS:可以在创建的时候传默认配置,比如staleTime、cacheTime 和 refetchOnWindowFocus,可以避免在每个 useQuery 调用中重复设置相同的选项。
useQuery({queryKey,queryFn,...queryOptions })
是 React Query 库中的一个钩子(Hook),用于从服务器获取数据,并在 React 组件中展示这些数据。它简化了数据获取、缓存、更新和错误处理的流程,使得开发者能够更加专注于业务逻辑的实现。
queryKey【查询key】
-
是一个数组,可序列化
-
简单形式
-
queryKey可以由一个字符串组成,用于标识基本的查询。例如:
useQuery({ queryKey: ['userData'], ... })。
-
-
复杂形式
-
当查询需要更多的信息来唯一描述数据时,queryKey可以包含多个字符串或可序列化对象。例如,根据userId查询用户信息时,可以使用:
useQuery({ queryKey: ['userInfo', userId], ... })。
-
-
-
是标识查询的唯一值
-
当queryKey是变量时,构成了查询依赖,只要变量值改变就会触发查询,类似于useEffect中的依赖项
-
使用场景
-
数据缓存:通过使用queryKey,React Query能够自动缓存查询结果。当相同的查询(即queryKey相同)再次发生时,React Query会直接使用缓存中的数据,从而提高性能。
-
数据更新:当查询条件发生变化时(例如分页、排序、过滤等),可以通过改变queryKey来触发新的查询。React Query会根据新的queryKey重新向服务器发送请求,并更新缓存数据。
-
数据同步:React Query支持自动或手动更新数据。当后台数据发生变化时,可以通过调用相关方法(如
invalidateQueries)来使匹配的查询失效,并触发新的数据获取和缓存更新。
-
// 当前页码,改变页码,请求就会更新
const [page, setPage] = React.useState(0)
// 请求函数
const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json())
const {
isLoading,
isError,
error,
data,
isFetching,
isPreviousData,
} = useQuery({
queryKey: ['projects', page],
queryFn: () => fetchProjects(page),
// 使用这个配置,可以防止切换页请求时,频繁的`loading`切换
keepPreviousData : true //在获取到新数据之前保持原有数据,优化用户体验
})
queryFn【查询函数】
-
queryFn就是一个普通的异步函数(promise函数)
-
返回的值会作为data放入,默认会给这个函数注入一些参数
-
resolve就会正常返回,thorw错误就会导致status = error
queryOptions
-
缓存和重新获取
-
enabled-
boolean -- 默认true
-
如果设置为 false,查询将不会执行,也不会从缓存中获取数据
-
-
大部分情况都没必要设置false,请求需要后续的某个操作触发,一定有变量变化,监听这个变化就行了
-
retry-
number | false | RetryFunction -- 默认3,重试3次
-
如果查询失败,是否自动重试,以及重试的次数或自定义的重试逻辑
-
-
refetchOnMount-
boolean -- 默认true
-
组件挂载时是否自动重新获取数据
-
-
refetchOnWindowFocus-
boolean -- 默认true
-
当用户切换回包含该组件的窗口或标签页时,是否自动重新获取数据
-
-
cacheTime-
number -- 默认Infinity
-
查询结果在缓存中保留的时间
-
-
更新和轮询
-
staleTime-
number -- 默认0
-
查询结果的staleTime过期时,React Query会将其标记为陈旧,并在后台尝试重新获取数据。然而,用户看到的仍然是旧数据,直到新数据到达并被更新到UI上。
-
-
pollingInterval-
number -- 默认0
-
轮询间隔时间
-
-
-
错误处理
-
onError 回调函数
-
-
其他选项
-
select-
(data: any) => any
-
用于在查询成功后,对从服务器获取的数据进行转换或处理的函数。允许你在将数据传递给组件之前,对其进行格式化、过滤或映射等操作。
select只有在data存在的时候才会被调用,不用担心它是undefined。
-
-
placeholderData-
查询时展示的临时数据的对象
-
-
initialData-
初始展示的数据对象,和
placeholderData的区别是,他会被缓存
-
-
notifyOnChange-
boolean | 'always' | ((prevData: any, nextData: any) => boolean),对于引用类型的变化是
true,对于原始类型的变化是false -
控制是否在数据变化时通知客户端
-
-
keepPreviousData-
boolean -- 默认true
-
如果查询失败,是否保留上一次成功查询的结果
-
-
并发请求
React.Suspense 主要用于处理组件的异步加载,通常与动态导入(React.lazy)结合使用来代码分割和按需加载组件。然而,React.Suspense 并不直接支持或管理多个并发请求的加载状态。
function App () {
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams })
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects })
}
串行请求
有的请求需要前一个请求完成后再请求,或者需要等到某个数据有值后再请求,就可以使用enabled配置控制
// 先请求用户
const user = useQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
const userId = user.data?.id
// 再请求项目
const projects = useQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
// 当userId存在的时候才会请求
enabled: !!userId,
})
请求状态
-
fetchStatus绑定请求函数的状态-
fetching -- isFetching
-
pause -- isPause
-
idle -- isIdle
-
-
status绑定请求数据状态-
loading -- isLoading
-
error -- isError
-
success -- isSuccess
-
isFetching?isLoading?
isFetching表示是否正在后台刷新数据,loading表示是否在后台获取数据
-
refetch-
类型:
() => Promise<void> -
描述:一个函数,调用它会重新触发查询,并从服务器获取新数据。
-
-
remove-
类型:
() => void -
描述:一个函数,调用它会从缓存中移除该查询及其数据。
-
useMutation({mutationFn:()=>{}})
useMutation 是 React Query 提供的一个钩子,用于处理数据变更操作(如创建、更新、删除等)。与 useQuery 不同,useMutation 不会自动执行,而是需要手动触发。它非常适合处理表单提交、数据更新等场景。
用法
-
定义突变函数
-
数据变更操作异步函数
-
-
使用
useMutation-
使用
useMutation钩子,并传入这个异步函数。useMutation将返回一个对象,该对象包含多个属性和方法,用于处理突变的状态和副作用。
-
-
触发突变
-
通过调用
mutate方法来触发数据变更操作。你可以传递任何需要的参数给mutate方法,这些参数将被传递给你的异步函数。
-
-
处理状态和错误
-
useMutation返回的对象包含状态信息,如isPending(是否正在加载)、isError(是否发生错误)以及error(错误信息)。你可以使用这些信息来向用户显示加载状态、错误消息等。
-
-
重置和清理
-
你可以使用
reset方法来重置突变的状态,或者在组件卸载时使用onSettled或onError等回调来执行清理操作。
-
...
const createPost = async (newPost) => {
// 异步请求返回数据
};
const MutationDemo = () => {
// 使用 useMutation 处理数据创建
const mutation = useMutation(createPost, {
onSuccess: (data) => {
// 数据获取成功
// 进行数据更新操作...
},
onError: (error) => {
// 数据获取失败
},
onMutate:()=>{
// 乐观更新
// 适用场景:实时性要求高的应用,高交互性应用...
}
});
const handleSubmit = (e) => {
e.preventDefault();
// 触发 mutation
mutation.mutate({ params });
// mutation包含数据触发突变的方法和状态,例如isLoading、isError
};
// 渲染,操作触发 handleSubmit-->突变
};
useInfiniteQuery({queryKey,queryFn,...queryOptions }) 【无限请求、滚动加载】
结合InfiniteScroll from "react-infinite-scroll-component";可以实现无限滚动查询
-
queryKey: 查询的唯一标识符。
-
queryFn
-
异步函数,用于获取数据。这个函数可以接受一个参数
pageParam,表示当前页码。
-
-
initialPageParam 获取第一页时要使用的默认页面参数
-
getNextPageParam
-
当接收到该查询的新数据时,该函数将接收无限数据列表的最后一页和所有页面的完整数组,以及pageParam信息。
-
它应该返回一个变量,该变量将作为最后一个可选参数传递给查询函数。
-
返回undefined或null表示没有可用的下一页。
-
-
queryOptions
-
getPreviousPageParam: 一个函数,用于从第一页的数据中获取上一页的 pageParam
-
onSuccess, onError 等
-
返回的data中有什么数据?
-
pages
-
类型:
Array -
包含所有已加载的页面数据。
-
[
{ data: [item1, item2], nextPage: 2 }, // 第一页
{ data: [item3, item4], nextPage: 3 }, // 第二页
{ data: [item5, item6], nextPage: 4 }, // 第三页
]
-
pageParams
-
Array
-
包含每页的
pageParam值。[1, 2, 3]
-
-
isFetchingNextPage: boolean
-
类型:
boolean -
表示是否正在加载下一页数据。
-
当
fetchNextPage被调用时,isFetchingNextPage会变为true,直到数据加载完成。
-
-
isFetchingPreviousPage: boolean
import InfiniteScroll from 'react-infinite-scroll-component';
import { useInfiniteQuery } from 'react-query';
// 1. 定义获取数据的函数
function fetchData({ pageParam = 1 }) {
...
}
function InfiniteScrollComponent() {
// 2. 使用 useInfiniteQuery 获取分页数据
const {
data, // 所有页面的数据
fetchNextPage, // 加载下一页的函数
hasNextPage, // 是否还有更多数据
isFetching, // 是否正在加载
} = useInfiniteQuery('data', fetchData, {
getNextPageParam: (lastPage) => lastPage.nextPage, // 获取下一页的页码
});
// 3. 将所有页面的数据扁平化
const allData = data ? data.pages.flatMap(page => page.items) : [];
return (
<div>
{/* 4. 使用 InfiniteScroll 组件 */}
<InfiniteScroll
dataLength={allData.length} // 当前已加载的数据长度
next={fetchNextPage} // 加载更多数据的函数
hasMore={hasNextPage} // 是否还有更多数据
loader={<div>Loading...</div>} // 加载中的提示
endMessage={<div>No more data!</div>} // 数据加载完毕的提示
>
{/* 5. 渲染数据 */}
<ul>
{allData.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</InfiniteScroll>
{/* 6. 加载状态反馈 */}
{isFetching && <div>Loading initial data...</div>}
</div>
);
}
//useInfiniteQuery 返回的数据是按页存储的,结构类似下面的
//滚动加载中,通常需要将所有数据合并为一个
{
pages: [
{ data: [item1, item2, item3], nextPage: 2 }, // 第一页
{ data: [item4, item5, item6], nextPage: 3 }, // 第二页
{ data: [item7, item8, item9], nextPage: 4 }, // 第三页
],
pageParams: [1, 2, 3], // 每页的参数
}
queryClient.prefetchQuery
-
作用:
-
prefetchQuery允许在实际需要数据之前预先获取数据并缓存。 -
这可以显著减少等待时间,提高应用程序的响应速度。
-
-
实用性:
-
在用户可能需要查看某个页面或组件之前,预先获取数据。
-
在用户将鼠标悬停在链接上时,预先获取链接指向页面的数据。
-
四、demo
import React, { useState } from 'react';
import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
// 模拟 API 调用
const fetchTodos = async () => {
return [
{ id: 1, text: 'Todo 1', completed: false },
{ id: 2, text: 'Todo 2', completed: false },
{ id: 3, text: 'Todo 3', completed: true },
];
};
const updateTodo = async (todoId, updatedTodo) => {
// 这里可以添加实际的 API 请求逻辑
return updatedTodo;
};
const addTodo = async (newTodo) => {
// 生成一个新的 ID(在实际应用中,通常从后端获取)
const id = Math.max(...(await fetchTodos()).map(todo => todo.id)) + 1;
const newTodoWithId = { ...newTodo, id };
// 这里可以添加实际的 API 请求逻辑
return newTodoWithId;
};
const deleteTodo = async (todoId) => {
// 这里可以添加实际的 API 请求逻辑
return true;
};
const TodoList = () => {
const queryClient = useQueryClient();
const { isLoading, error, data: todos } = useQuery(['todos'], fetchTodos);
const addTodoMutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries(['todos']);
},
});
const updateTodoMutation = useMutation(updateTodo, {
onSuccess: () => {
queryClient.invalidateQueries(['todos']);
},
});
const deleteTodoMutation = useMutation(deleteTodo, {
onSuccess: () => {
queryClient.invalidateQueries(['todos']);
},
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
return (
<div>
<h1>Todo List</h1>
<input
type="text"
placeholder="Add new todo"
onKeyPress={(e) => {
if (e.key === 'Enter') {
addTodoMutation.mutate({ text: e.target.value, completed: false });
e.target.value = '';
}
}}
/>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={(e) => {
updateTodoMutation.mutate({ ...todo, completed: e.target.checked }, {
select: (updatedTodo) => updatedTodo,
});
}}
/>
{todo.text}
<button onClick={() => deleteTodoMutation.mutate(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
};
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<TodoList />
</QueryClientProvider>
);
}
export default App;
[警告]&[问号]
在使用 React Query 的 useQuery 时,select 选项用于对查询返回的数据进行转换或处理。它允许你在数据返回后,对数据进行格式化、过滤或其他操作。然而,当你使用 setQueryData 手动更新缓存数据时,需要特别注意 数据格式的统一性,手动将数据转换为与 select 处理后的格式一致。
-
select用于对查询返回的数据进行处理,返回一个新的数据结构。select只有在data存在时才会触发。如果查询尚未完成或数据不存在,select不会被调用。因此,你不需要在select函数中额外处理data不存在的情况。 -
setQueryData用于手动更新缓存中的数据。它直接修改缓存中的数据,不会触发select逻辑。
isSuccess 表示查询成功完成,但并不保证 data 立即包含最新的数据。在某些情况下,由于网络延迟或缓存机制,data 可能需要一些时间才能更新如果您需要确保数据是最新的,
-
可以使用
isFetching状态。表示查询正在获取数据,包括初始获取和后台重新获取。您可以在isFetching为false且isSuccess为true时执行数据处理逻辑。 -
添加
data数据的判断
enable,变为true,useQuery 会立即发起网络请求,获取数据。enable,变为false,会停止正在进行的网络请求(如果存在),查询数据会被缓存,但不会再进行后台更新。
在 TanStack Query(以前称为 React Query)中,当 queryKey 包含对象或数组时,默认情况下会进行浅比较。这意味着:
-
引用地址比较:
-
TanStack Query 会比较对象或数组的引用地址,而不是它们的内容。
-
如果对象或数组的内容发生变化,但引用地址保持不变,TanStack Query 不会检测到
queryKey的变化。
-
-
不会触发查询:
-
因此,如果
queryKey包含一个对象,并且该对象内部的值发生了变化,但对象的引用地址没有改变,那么 TanStack Query 不会触发查询的重新获取。
-
const [filters, setFilters] = useState({ status: 'active' });
const { data, isLoading } = useQuery({
queryKey: ['tasks', filters],
queryFn: () => fetchTasks(filters),
});
const updateFilters = () => {
filters.status = 'completed';
setFilters({ ...filters });
};
// 由于我们直接修改了 filters 对象内部的值,它的引用地址并没有改变。也不会触发 fetchTasks 函数
// 但是如果使用setFilters({ ...filters }); 这样就可以生成新的对象,引用地址会发生变化,从而触发查询。
// 可以使用扩展运算符(...)或 Object.assign() 来创建新的对象。
// 如果对象中的字段很多,或者嵌套很深,可以使用JSON.stringify()将对象序列化为字符串
// 可以使用 useMemo() 来缓存对象或数组,以避免不必要的重新创建
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)