查询 queries
对于query的定义,在官方文档中是这么说的:
“A query is a declarative dependency on an asynchronous source of data that is tied to a unique key” 翻译过来是:查询是对绑定到唯一键的异步数据源的声明性依赖项
我对于query的理解是:将一个唯一的key(unique key) 与 一个获取数据的方法 进行绑定
你可以在组件中或者hook中,使用 useQuery
来订阅一个查询
useQuery
至少需要接受两个参数:
- key:该查询的唯一键值
- fn:一个返回Promise的函数,能解析出数据或者抛出错误(即具体的请求数据的函数)
import { useQuery } from '@tanstack/react-query'
function App() {
// 具体获取数据的函数
const getUserInfo = async () => {
const data = await axios('/api/useInfo')
return data
}
// 订阅一个查询
const userInfo = useQuery({
queryKey: ['useData'],
queryFn: getUserInfo
})
}
这个唯一的key值,将在内部用于重新获取、缓存和在整个项目中共享该查询的信息
useQuery
的返回结果包含了你需要用到的绝大部分信息,这也正是使用react-query的一个好处,可以提高开发效率
// 接上面例子
const { isLoading, isError, isSuccess, data, error, status } = useQuery({
queryKey: ['useData'],
queryFn: getUserInfo
})
首先是结果中会返回一些比较重要的状态:
- 布尔值
isLoading
或者status === 'loading'
表示查询暂时还没有数据 - 布尔值
isError
或者status === 'error'
表示查询遇到了错误 - 布尔值
isSuccess
或者status === 'success'
表示查询成功,数据可用
可以看出 status 和 isLoading、isError、isSuccess的作用是差不多的,关键在于你习惯于使用谁来进行判断
然后是一些重要的信息:
error
如果查询处于isError
状态,可以获取到错误的具体信息data
如果查询处于isSuccess
状态,可以获取到请求的数据
下面是一个简单的例子,来整体演示使用useQuery
import { useQuery } from '@tanstack/react-query'
function App() {
// 具体获取数据的函数
const getUserInfo = async () => {
const data = await axios('/api/useInfo')
return data
}
// 订阅一个查询
const { isLoading, isError, isSuccess, data, error, status } = useQuery({
queryKey: ['useData'],
queryFn: getUserInfo
})
if (status === "loading") {
return <span>Loading...</span>;
}
if (status === "error") {
return <span>Error: {error.message}</span>;
}
return (
<div>
<div>userName: {data.userName}</div>
<div>age: {data.age}</div>
</div>
)
}
查询键值 queryKey
在react-query内部基于queryKey来管理查询缓存
queryKey必须是一个数组,对数组内部的结构没有过多的限制,该数组可以简单的由一个或多个字符串构成,也可以是包含许多嵌套对象的数组。
- 最简单的形式
useQuery({
queryKey: ['userData'],
...
})
- 复杂的形式
当查询需要更多的信息来唯一的描述数据时,数组可以是 字符串 加上 任意数量的可序列化对象的形式
常见的场景有:需要传递参数来进行查询
// 根据 userId 查询数据
let userId = 'xxx'
useQuery({
queryKey: ['useInfo', userId],
...
})
// 查询特定type的数据
useQuery({
queryKey: ['xxxx', { type: 'xxx' }]
})
注意:
queryKey的散列是确定的,即 顶层数组中各个key不同的排列顺序会被认为是不同的查询键值
// 以下两个的查询键值不相等,因为顶层数组中各key的排列顺序不同
useQuery({ queryKey: ['todos', status, page], ... })
useQuery({ queryKey: ['todos', page, status], ...})
// 以下两个的查询键值是相等的,顶层数组中各key的排列顺序相同,嵌套对象中的排列顺序不影响
useQuery({ queryKey: ['todos', { status, page }], ... })
useQuery({ queryKey: ['todos', { page, status }], ...})
查询函数 queryFn
官方文档对queryFn的定义:A query function can be literally any function that returns a promise. The promise that is returned should either resolve the data or throw an error.
即:queryFn可以是任何形式的函数,必须返回一个promise,并且返回的promise应该 给出数据 或者 抛出错误
- queryFn的参数
queryFn默认接受一个queryFunctionContext
参数,该参数是一个对象,主要包含以下属性:
- queryKey:即整个queryKey数组
- pageParam:在 无限查询 中会使用到,包含查询当前页所使用的参数
- signal:用作 查询取消
- meta:可以填写任意关于该查询的额外信息
由上可知,queryKey不仅可以用于唯一的标识查询,也可以作为参数传递给queryFn
useQuery({
queryKey: ['useData', useId],
queryFn: getUserData
})
const getUserData = async ({ queryKey }) => {
const [ _key, useId ] = queryKey
const data = await fetch('/api/useData' + useId)
return data
}
- 抛出和处理错误
为了使 React Query 确定查询错误,查询函数的错误必须抛出或返回rejected Promise。查询函数中引发的任何错误都将被持久化在查询的error状态中
并行查询
并行查询即并行的执行多个查询,或者说同时执行的查询
- 如果需要并行查询的数量较少且固定,可以使用手动的并行查询
function App () {
// 下面的查询将自动地并行执行
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams });
const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects });
...
}
在 React 的 suspense 模式下使用 React Query 时,这种并行模式不起作用。 因为第一个查询将在内部抛出 Promise,并且将在其他查询运行之前挂起组件。 此时建议使用
useQueries
hook
- 使用
useQueries
进行动态并行查询
如果需要执行的查询数量不固定,即在每次渲染之间都会变化,那就不能进行手动查询了
useQueries
接受一组作为查询配置的对象,并以数组形式返回查询的结果:
function App({ users }) {
const userQueries = useQueries({
queries: users.map((user) => {
return {
queryKey: ["user", user.id],
queryFn: () => fetchUserById(user.id),
};
}),
});
}
有依赖的查询
有依赖的查询 或者说 按顺序排列的查询,即当前查询是否执行(或何时执行)依赖于前一个查询的结果
可以使用enable
配置项来告诉query何时可以运行:
// 先执行这个query来拿到user的数据
const { data } = useQuery({
queryKey: ["user", email],
queryFn: getUserByEmail,
});
const userId = data?.id;
// 此query依赖于 user query 的结果
const { data } = useQuery({
queryKey: ["projects", userId],
queryFn: getProjectsByUser,
// 直到 userId 存在,查询才会被执行
enabled: !!userId,
});
QueryClient
使用QueryClient
可以创建一个query客户端,来与query缓存联系起来
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient({
// 全局设置query的一些配置
defaultOptions: {
queries: {
staleTime: Infinity,
},
},
})
QueryClient
可传入一个对象,其中包含三个参数(三个参数都是可选的):
defaultOptions
:为使用该client的所有查询(query)和修改(mutation)设置默认的配置项queryCache
:该client所连接的query缓存mutationCache
:该client所连接的mutation缓存
配置项
refetchOnWindowFocus
如果用户在短暂离开窗口后回来时,数据被标记为过时的,react-query会在后台自动请求新的数据
可以使用该配置项在全局或者单个查询中禁用该功能
// 全局
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
})
// 单个查询
useQuery({
queryKey: ["useData"],
queryFn: getUserData,
refetchOnWindowFocus: false,
})
enabled
可以为单个查询配置enabled = false
来禁用自动查询
当enabled = false
时:
- 如果查询已经缓存了数据,将以
status === 'success'
进行初始化 - 如果查询没有缓存数据,将以
status === 'loading'
进行初始化 - 该查询不会在挂载时自动获取数据、不会在后台重新获取数据
- 将忽略客户端的
invalidateQueries
和refetchQueries
调用 - 从
useQuery
返回的refetch
可用于手动触发查询以进行数据获取
// 手动触发查询
const { isError, data, error, refetch } = useQuery({
queryKey: ["useData"],
queryFn: getUserData,
enabled: false,
})
<button onClick={() => refetch()} >手动触发查询</button>
永久性的禁用查询并不是你使用react-query的理由,你可能更多的需要进行惰性查询
retry
当useQuery
查询失败时,如果该查询的请求未达到最大连续重试次数(默认 3 次),那么react-query将自动重试该查询。
可以在全局或者单个查询上配置重试逻辑
retry = false
将禁用重试 &&retry = true
将无限次重试retry = 5
设置为一个数字,表示最大重试次数retry =(failureCount,error)=> ...
允许基于请求失败的原因进行自定义逻辑
QueryClient的一些api
queryClient.fetchQuery
该方法是一个异步的方法,用于触发查询并将结果缓存,通常用于预加载数据。
参数
fetchQuery
接受的参数和useQuery
相同,即 queryKey、queryFn、option(可选)
返回值
调用fetchQuery
将返回一个promise对象,包含了请求错误的信息 或者 成功时的数据
当调用fetchQuery
时会执行如下步骤:
- 检查缓存中是否已有与提供的 queryKey 对应的缓存数据
- 如果缓存中有数据,并且数据是新鲜的,那么
fetchQuery
将返回这些数据。 - 如果缓存中没有数据,或者数据已过时,那么将执行 queryFn,并将结果数据放入缓存中。
使用示例
演示用户在导航到用户详情页时,使用fetchQuery
预加载用户数据
import { QueryClient } from 'react-query';
// 创建一个 QueryClient 实例
const queryClient = new QueryClient();
// 一个函数,用于获取用户数据
const fetchUserById = async (userId) => {
const response = await fetch(`api/users/${userId}`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
};
// 假设这是一个事件处理函数,当用户点击时触发
const handleUserLinkClick = (userId) => {
// 使用 fetchQuery 预加载用户数据
queryClient.fetchQuery(['user', userId], () => fetchUserById(userId));
// 在这里,我们可能会进行路由导航操作
navigate(`/users/${userId}`);
};
// 用户详情组件
const UserDetails = ({ userId }) => {
// 使用 useQuery 钩子查询用户数据
const { data, isLoading, error } = useQuery(
['user', userId],
() => fetchUserById(userId),
{
// 如果数据已经在 fetchQuery 中预加载,这里会立刻拿到缓存数据
staleTime: Infinity, // 你可以根据需要设置合适的新鲜度
}
);
if (isLoading) {
return <span>Loading...</span>;
}
if (error) {
return <span>Error: {error.message}</span>;
}
return (
<div>
<h1>{data.name}</h1>
{/* ... 其他用户信息 */}
</div>
);
};
queryClient.prefetchQuery
该方法是一个异步的方法,用于在后台提前获取数据并将其存储在缓存中,以便在未来某个时刻需要时能够立即使用
prefetchQuery
的工作方式与fetchQuery
大致相同,但prefetchQuery
更关注于数据的预加载,它通常不返回数据,只是单纯地将数据预加载到缓存中。同时,prefetchQuery
通常在数据需要之前调用,而不是在渲染组件时调用
queryClient.getQueryData
该方法是一个同步方法,用于返回已存在的查询的缓存数据,如果没有缓存数据则返回undefined
参数
只需要传入 queryKey 即可
返回值
如果缓存存在则返回数据,否则返回undefined
使用示例
const data = queryClient.getQueryData(queryKey)
更多
其他更多的queryClient
api 可以前往官网查看
主动查询失败 Query Invalidation
查询会在过时之后自动重新查询,但是在很多时候,由于修改了某些数据,你能明确的知道数据已经是过时的了(即使它还没有到默认的过时时间)。
这个时候可以调用QueryClient
的invalidateQueries
方法来明确的告诉react-query数据已经过时了,并重新查询新的数据
简单的例子:
import { useQuery, useQueryClient } from "@tanstack/react-query"
// 获取 queryClient
const queryClient = useQueryClient()
// 使缓存中的每个查询都无效
queryClient.invalidateQueries()
// 无效以 userData 开头的键值的查询
queryClient.invalidateQueries({ queryKey: ["userData"] })
对于传入queryKey使查询失效的精确度,也有多种方式进行控制
- 传入特定的(或者完整)的queryKey
queryClient.invalidateQueries({
queryKey: ["userData", { userName: "easy code sniper" }],
})
// 该查询会被无效
const userQuery = useQuery({
queryKey: ["userData", { userName: "easy code sniper" }],
queryFn: getUserData,
})
// 该查询不会被无效
const userQuery = useQuery({
queryKey: ["userData"],
queryFn: getUserData,
})
- 使用
exact
配置,表示只想使指定的queryKey对应的查询失效
queryClient.invalidateQueries({
queryKey: ["userData"],
exact: true,
})
// 该查询会被无效
const userQuery = useQuery({
queryKey: ["userData"],
queryFn: getUserData,
})
// 该查询不会被无效
const userQuery = useQuery({
queryKey: ["userData", { userName: "easy code sniper" }],
queryFn: getUserData,
})
- 自定义更精细化的查询失效
可以将 predicate
函数传递给invalidateQueries
方法。 此函数将从查询缓存中接收每个Query实例,并允许你返回 true 或 false 来确定是否使该查询无效
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === "userData" && query.queryKey[1]?.age >= 18,
});
// 该查询会被无效
const userQuery = useQuery({
queryKey: ["userData", { age: 20 }],
queryFn: getUserData,
});
// 该查询会被无效
const userQuery = useQuery({
queryKey: ["userData", { age: 10 }],
queryFn: getUserData,
});
修改 Mutations
对于mutations的定义,在官方文档中是这么说的:
“mutations are typically used to create/update/delete data or perform server side-effects.”
即:用于创建、删除、更新数据或者执行服务器命令等操作
你可以在组件或者自定义hook中使用 useMutation
来修改数据
简单示例:修改用户的信息
function App() {
const mutation = useMutation({
mutationFn: (userInfo) => {
return axios.post("/updateUserInfo", userInfo);
},
});
return (
<div>
{mutation.isLoading ? (
"updating ..."
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>updated!</div> : null}
<button
onClick={() => {
mutation.mutate({ userName: 'easy code sniper' });
}}
>
修改用户信息
</button>
</>
)}
</div>
);
}
由上面的例子可以看出,你可以通过调用mutation
方法来给mutationFn传入参数
useMutation
和查询一样也会返回一些状态和信息:
状态:
- 布尔值
isLoading
或者status === 'loading'
表示修改正在进行 - 布尔值
isError
或者status === 'error'
表示修改遇到了错误 - 布尔值
isSuccess
或者status === 'success'
表示修改成功,数据可用
信息:
error
如果修改处于isError
状态,可以获取到错误的具体信息data
如果修改处于isSuccess
状态,可以获取到数据
注意:
mutate
函数是一个异步函数,在React16及以前版本,你不能在事件回调中直接使用它。你需要将mutate包装在另一个函数中
// 在React16及之前的版本,这将无法正常工作
const CreateTodo = () => {
const mutation = useMutation({
mutationFn: (event) => {
event.preventDefault()
return fetch("/api", new FormData(event.target))
},
})
return <form onSubmit={mutation.mutate}>...</form>
}
// 这将正常工作
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>
}
副作用
useMutation
最便利,也是最有用的功能可能就在于它能定义一些副作用配置,这些配置允许在其生命周期的任何阶段快速而简单地产生副作用。
一个最常见的例子就是,在修改数据之后能自动的重新获取最新的数据(如果你经历过一些开发,那你一定会对这个功能感到兴奋🚀~~)
useMutation({
mutationFn: updateUserInfo,
onMutate: (variables) => {
// 修改即将发生!
},
onError: (error, variables, context) => {
// 错误触发!
},
onSuccess: (data, variables, context) => {
// 成功时触发
},
onSettled: (data, error, variables, context) => {
// 错误或成功……这并不重要
},
})
除了在useMutation
中可以配置这些副作用项外,还可以在调用mutate
函数时配置组件特定的副作用,支持的配置项包括:onSuccess
、onError
和 onSettled
mutate(userInfo, {
onSuccess: (data, variables, context) => {
},
onError: (error, variables, context) => {
},
onSettled: (data, error, variables, context) => {
},
})
注意: 如果组件在修改完成之前就被卸载了,那使用mutate配置的组件特定的副作用将不被运行
修改导致的查询失败
在开发中非常常见的是,当应用中的一个对数据的修改成功时,很有可能在你的应用中有相关的查询需要作废,并需要重新获取数据来解释修改所产生的新变化
假设我们调用const mutation = useMutation({ mutationFn: updateUserInfo })
对用户信息进行了修改,那么势必导致现在渲染的数据是过时的。我们希望所有对userInfo的查询都失效,并重新获取最新的数据
我们可以使用useMutation
的副作用onSuccess
配置 和QueryClient的invalidateQueries
函数来实现
import { useMutation, useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();
// 当此修改成功时,将所有带有useData查询键值的查询都无效
const mutation = useMutation({
mutationFn: updateUserInfo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["userData"] })
},
})
通过修改的数据更新查询内容
当在处理更新某些数据时,新的数据往往会在更新的响应中自动返回,我们可以利用修改函数返回的对象,并使用 Query Client 的 setQueryData
方法立即用新数据更新现有的查询,而不是去触发新的数据获取,浪费对已有数据的网络调用
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateUserInfo,
onSuccess: (data) => {
queryClient.setQueryData(["userData", { userName: 'cqy' }], data);
},
});
mutation.mutate({
userName: 'cqy',
age: 22,
});
// 下面的查询将被更新为成功的修改响应
const { status, data, error } = useQuery({
queryKey: ["useData", { userName: 'cqy' }],
queryFn: fetchTodoById,
});
useInfiniteQuery
useInfiniteQuery
钩子用于处理无限滚动或分页场景,它可以用来逐页加载数据,并且可以无缝地集成更多数据加载到现有数据集中。
参数
useInfiniteQuery
接受以下参数:
- queryKey
- queryFn
- options (可选): 一个配置对象,包含以下属性:
getNextPageParam
: 一个函数,用于从最后一页的数据中获取下一页的 pageParamgetPreviousPageParam
: 一个函数,用于从第一页的数据中获取上一页的 pageParam- 其他 React Query 提供的所有配置选项,如 staleTime, cacheTime, onSuccess, onError 等
getNextPageParam
getNextPageParam
用于定义如何从获取到的数据中提取分页参数,以便加载下一页的数据。
getNextPageParam
函数接收两个参数:
lastPage
: 当前查询返回的最后一页数据。allPages
: 当前已经加载的所有页面组成的数组。
函数的返回值应该是一个值,这个值将作为下一个 pageParam
参数传递给 queryFn
函数以获取下一页数据。
示例:
const getNextPageParam = (lastPage, allPages) => {
// 如果 API 有一个 'nextPage' 字段,就返回它来获取下一页
return lastPage.nextPage;
};
const getNextPageParam = (lastPage, allPages) => {
// 如果 API 使用游标,并且存在一个 'nextCursor' 字段,就返回它
return lastPage.nextCursor;
};
getPreviousPageParam
getPreviousPageParam
用于获取前一页数据,在实现双向无限滚动或分页时特别有用。
getPreviousPageParam
函数接收两个参数:
firstPage
: 当前查询返回的第一页数据。allPages
: 目前已经加载的所有页面数据的数组。
这个函数的返回值应该是一个标识,表示用于queryFn
函数获取前一页数据时所需的pageParam
示例:
const getPreviousPageParam = (firstPage, allPages) => {
// 如果 API 有一个 'prevPage' 字段,就返回它来获取前一页
return firstPage.prevPage;
};
const getPreviousPageParam = (firstPage, allPages) => {
// 如果 API 使用游标,并且存在一个 'prevCursor' 字段,就返回它
return firstPage.prevCursor;
};
返回值
useInfiniteQuery
返回一个对象,其中包含以下属性:
status
: 查询的状态(‘loading’, ‘error’, ‘success’, ‘idle’)。data
: 包含每一页数据的数组。error
: 查询失败时的错误对象。fetchNextPage
: 函数,用于获取下一页数据。fetchPreviousPage
: 函数,用于获取上一页数据。hasNextPage
: 布尔值,表示是否还有下一页数据。hasPreviousPage
: 布尔值,表示是否还有上一页数据。isFetchingNextPage
: 布尔值,表示是否正在获取下一页数据。isFetchingPreviousPage
: 布尔值,表示是否正在获取上一页数据。isFetching
: 布尔值,表示是否正在进行查询(包括初始查询和后续的分页查询)。isLoading
: 布尔值,表示是否正在进行初始查询。isRefetching
: 布尔值,表示是否正在重新获取数据。- 其他 React Query 钩子通常返回的属性,如 refetch, remove 等。
data
useInfiniteQuery
返回的data与常规的useQuery
不同,因为它需要处理多个“页”的数据,而不是单个数据集。
data
是一个对象,它包含以下属性:
pages
: 一个数组,其中的每一个元素代表了每一页加载的数据。这些数据按照加载的顺序排列。例如,data.pages[0]
是第一页的数据,data.pages[1]
是第二页的数据,依此类推。pageParams
: 这个数组与pages
数组有相同的长度,包含了获取每一页数据时 queryFn 函数使用的参数。通常这些参数是由getNextPageParam
或getPreviousPageParam
函数提供的。
使用示例
下面是一个使用 useInfiniteQuery
来实现无限滚动加载文章列表的例子:
import { useInfiniteQuery } from 'react-query';
// 分页获取文章的函数
const fetchArticles = async ({ pageParam = 1 }) => {
const res = await fetch(`/api/articles?page=${pageParam}`);
return res.json();
};
// 组件内部
const ArticlesList = () => {
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
} = useInfiniteQuery('articles', fetchArticles, {
getNextPageParam: (lastPage, pages) => {
if (lastPage.nextPage) {
return lastPage.nextPage;
} else {
return undefined;
}
},
});
if (status === 'loading') return <p>Loading...</p>;
if (status === 'error') return <p>Error: {error.message}</p>;
return (
<>
{data.pages.map((page, i) => (
<React.Fragment key={i}>
{page.articles.map(article => (
<p key={article.id}>{article.title}</p>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
</>
);
};
useMutation
useMutation
钩子是用来处理异步逻辑(如创建、更新或删除数据)的,这些逻辑会引起数据的变化。与useQuery
和useInfiniteQuery
不同的是,useMutation
不是用来获取数据的,而是用来修改数据
参数
useMutation
接受以下参数:
mutationFn
(必须): 一个函数,它执行异步逻辑(比如API调用)。这个函数接受你传递给mutate函数的变量。options
(可选): 一个配置对象,可以包含如下属性:onMutate
: 在 mutationFn 执行之前调用的函数,用于执行乐观更新或返回回滚函数的数据。onSuccess
: 当 mutationFn 成功完成时调用的函数。onError
: 当 mutationFn 执行失败时调用的函数。onSettled
: 无论 mutationFn 成功还是失败都会调用的函数。- 以及其他选项
返回值
useMutation
返回一个对象,其中包含以下属性和方法:
mutate
: 一个函数,你可以用它来触发异步逻辑(mutationFn)的执行。mutateAsync
: 类似于mutate,但是返回一个Promise,可以用于async/await。data
: mutationFn成功解析的数据。error
: 如果mutationFn抛出错误,这里会包含错误对象。isLoading
: 如果mutationFn正在执行,这里会是true。isSuccess
: 如果mutationFn成功完成,这里会是true。isError
: 如果mutationFn执行失败,这里会是true。status
: mutation的状态(idle、loading、success、error)。
最后
在我的博客上分享了更多的学习心得,感兴趣的小伙伴可以去看看