在上一章中,介绍了
QueryClient
和React Query Devtools
,接下来将结合实际工作场景来讲述 React Query 的最佳实践和一些思考~
长文预警!!useQuery 作为 React Query 最核心的 API 之一,必须要好好深究一番!
本章代码放在我的
github repo
:ian-kevin126/react-query-best-practices
在 React Query 中,可以使用 useQuery
来获取、缓存和处理服务端状态。为了缓存数据,React Query 构建了一个叫 queryKey
的东西,结合其他的一些配置,React Query
可以将服务端状态管理提升到一个新的水平。还可以使用它在某些情况下重新获取数据和解决相互依赖的查询。
useQuery
在 React Query 文档 中,查询是这样定义的:查询是对异步数据源的声明式依赖,并且它与唯一 key 绑定。
可以这样使用:
jsx
import { useQuery } from "@tanstack/react-query";
const values = useQuery({
queryKey: <insertQueryKey>,
queryFn: <insertQueryFunction>,
});
- queryKey:用于标识查询的唯一
key
,老版本可以设置为字符串,v4 之后推荐用数组; - queryFn:返回
Promise
的函数;
queryKey
queryKey
是 React Query 用来标识查询的唯一值。此外,React Query 还会使用 queryKey
将数据缓存到查询缓存(QueryCache
)中,还能通过它手动处理查询。
queryKey
需要是一个数组(为了兼容性,依然还可以设置成字符串,但是推荐还是数组吧,说不定以后不支持字符串方式了 ),其中可以只包含一个字符串,也可以包含其他值,例如对象。需要注意的是,queryKey
数组中的值需要是可序列化的。
PS:在
React Query v4
之前,queryKey
并不一定需要是一个数组。它可以只是一个字符串,因为 React Query 会在内部将其转换为数组。所以,如果你在网上看到一些字符串作为queryKey
的示例,不要觉得奇怪。
下面是一些 queryKey
的示例:
jsx
useQuery({ queryKey: ['users'] })
useQuery({ queryKey: ['users', 10] })
useQuery({ queryKey: ['users', 10, { isVisible: true }] })
useQuery({ queryKey: ['users', page, filters] })
为了使你的 queryKey
唯一,并且 useQuery
钩子更可读,建议将查询的所有参数作为 queryKey
的一部分添加进去。就像 useEffect 上的依赖关系数组类似的那种心智模型,因为 queryKey
还允许 React Query 在查询的参数发生变化时,可以自动重新发起查询。
需要注意的一点是,
queryKey
是会被确定性散列化的,也就是说数组内项的顺序很重要,可以在官网、源码中找到。那什么是确定性散列?其实散列也叫 hash(哈希)。
- 确定性哈希 对于数据完整性验证很重要,通过对数据应用哈希函数,可以生成哈希值,该值充当该数据的数字指纹,具备唯一性。
比如,下面是一些查询, queryKey
在确定性散列化时将被视为是同一个查询:
jsx
useQuery({ queryKey: ['users', 10, { page, filters }] })
useQuery({ queryKey: ['users', 10, { filters, page }] })
useQuery({ queryKey: ['users', 10, { page, random: undefined, filters }] })
这些示例都是同一个查询 ------ 因为在这三个示例中,queryKey
中数组的顺序是不变的。
你可能想知道这怎么可能?因为 page 和 filter 的位置每次都会改变,而且在最后一个示例中,还有第三个叫 random
的属性。
但事实上,它们仍在同一个对象内,而该对象在 queryKey
数组中的位置没有改变。此外, random
属性是 undefined
的,因此在进行散列(hashing)时,它会被忽略。
而下面这些散列化时就不是同一个查询:
jsx
useQuery({ queryKey: ['users', 10, undefined, { page, filters }] })
useQuery({ queryKey: ['users', { page, filters }, 10] })
useQuery({ queryKey: ['users', 10, { page, filters }] })
你可能想知道为什么第一个示例与最后一个示例不一样,放在对象内和对象外区别这么大吗?因为顺序很重要,散列后,这个 undefined
将转化为散列键内的空值。
对于一些比较规范的 CRUD
接口请求方法,我们可以像下面这样将 queryKey
封装起来:
jsx
const todoKeys = {
all: ['todos'] as const,
lists: () => [...todoKeys.all, 'list'] as const,
list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
details: () => [...todoKeys.all, 'detail'] as const,
detail: (id: number) => [...todoKeys.details(), id] as const,
}
这样可以带来很好的灵活性,比如:
jsx
queryClient.removeQueries({ queryKey: todoKeys.all })
queryClient.invalidateQueries({ queryKey: todoKeys.lists() })
queryClient.prefetchQueries({
queryKey: todoKeys.detail(id),
queryFn: () => fetchTodo(id),
})
queryFn
queryFn
需要是一个返回 Promise
的函数,这使得 React Query 变得更加强大,因为 queryFn 可以支持任何能够执行异步数据获取的 client:比如 REST
和 GraphQL
。
比如在 GraphQL
中:
jsx
import { useQuery } from "@tanstack/react-query";
import { request, gql } from "graphql-request";
const customQuery = gql`
query {
posts {
data {
id
title
}
}
}
`;
const fetchGQL = async () => {
const { posts: { data }, } = await request('xxx/sss', customQuery);
return data;
};
// ...
useQuery({
queryKey: ["posts"],
queryFn: fetchGQL
});
在 RESTful API
中:
jsx
import axios from "axios";
const fetchData = async () => {
const { data } = await axios.get('https://xxx.aaa.com/api/sss');
return data;
};
// ...
useQuery({
queryKey: ["api"],
queryFn: fetchData,
});
为了让 React Query
可以正确处理错误场景,在使用这些 client
时需要注意的一点是,它们是否会在请求失败时自动抛出错误。如果它们不抛出错误,你就必须自己抛出错误。
你可以在使用 fetch 的 queryFn
中这样做:
jsx
const fetchDataWithFetch = async () => {
const response = await fetch('https://xxx.aaa.com/api/sss')
// 检查响应是否成功。如果无效,我们会抛出一个错误
if (!response.ok) throw new Error('Something failed')
return response.json()
}
在封装构建查询函数中,也可以将 queryKey
传递给 queryFn
,因为 queryKey 中包含了一些我们传的查询参数,传给 queryFn 之后,React Query 会在这些参数变化时自动取重新请求数据。
官方给了两种方式:
- 内联函数(Inline function)
- 查询函数Context(QueryFunctionContext)
内联函数
当查询键中需要传递给查询函数的参数不多时,就可以利用这种模式。来看一个示例:
jsx
import axios from "axios";
import { useQuery } from "@tanstack/react-query";
const fetchData = async (nameOrId) => {
const { data } = await axios.get(
`https://dummyjson.com/products/${nameOrId}`
);
return nameOrId ? { products: [data] } : data;
};
export const useFetchData = (nameOrId) => {
return useQuery({
queryKey: ["api", nameOrId],
queryFn: () => fetchData(nameOrId),
});
};
然后在 App.js 中使用:
js
import { useState } from "react";
import { useFetchData } from "./api";
import { Button, Table, Input } from "antd";
function App() {
const [nameOrId, setNameOrId] = useState("");
const { data } = useFetchData(nameOrId);
const handleOnChangeName = (e) => {
setNameOrId(e?.target?.value);
};
const columns = [
{
title: "名 称",
dataIndex: "title",
key: "title",
},
{
title: "价 格",
dataIndex: "price",
key: "price",
},
{
title: "品 牌",
dataIndex: "brand",
key: "brand",
},
];
return (
<div className="App">
<div style={{ display: "flex", alignItems: "center" }}>
<Input value={nameOrId} onChange={handleOnChangeName} />
<Button type="primary">搜索</Button>
</div>
<Table
dataSource={data?.products ?? []}
columns={columns}
rowKey={"id"}
/>
</div>
);
}
export default App;
测试一下,打开页面,在不输入任何参数的情况下,会请求全量的数据:
在输入框中输入一个id,你就会发现触发了接口的重新请求了对应id的数据:
随意改变id的值,后台都会自动取请求接口数据,无需再去点击一次搜索按钮!是不是很方便?不需要我们手动再去做了,这在一些管理后台的列表页中非常好用~
QueryFunctionContext
在参数比较少的时候,上面这种模式非常适合。但是,如果你有十几个参数,而所有参数都需要在查询函数中使用。这种做法也不是不行,但会影响代码的可读性。为了避免这种情况,你可以使用 QueryFunctionContext
对象。
每次调用查询函数时,React Query 都会自动将 queryKey
作为 QueryFunctionContext
对象传递给 queryFn
。
下面是一个使用 QueryFunctionContext 模式的示例:
jsx
import axios from "axios";
import { useQuery } from "@tanstack/react-query";
import qs from "qs";
const fetchData = async ({ queryKey }) => {
const [_queryKeyIdentifier, params] = queryKey;
console.log("🚀 ~ file: index.js:14 ~ fetchData ~ params:", params);
const { data } = await axios.get(
`https://dummyjson.com/products?${qs.stringify(params)}`
);
return data;
};
export const useFetchData = (params) => {
return useQuery({
queryKey: ["api", params],
queryFn: fetchData,
});
};
我们首先创建了 fetchData
函数。该函数将接收 QueryFunctionContext
作为参数,因此我们可以立即从该对象中解构 queryKey
,进而解构到我们传入的参数。
然后将参数拼接到用于获取数据的 URL,这样,我们只需改变查询参数,后台就会自动发起接口请求,无需手动。
这种模式要求将查询的所有参数项添加到
queryKey
中,有一个缺点,那就是由于参数太多,你必须记住将它们添加到queryKey
的顺序,才能在queryFn
中使用它们。解决这个问题的方法之一是发送一个包含queryFn
中所需全部参数的对象。这样就无需记住数组元素的顺序了。
具体方法如下:
jsx
useQuery({
queryKey: [{queryIdentifier: "api", someVariable}],
queryFn: fetchData,
});
通过传递一个对象作为 queryKey
,该对象将作为 QueryFunctionContext 对象发送到 queryFn
。然后,在接口请求函数中,你只需:
jsx
const fetchData = async ({ queryKey }) => {
const { someVariable } = queryKey[0];
// ...
};
我们从 QueryFunctionContext 对象中重组了 queryKey
。然后,由于我们的对象将位于 queryKey
的第一个位置,我们可以从我们的对象中解构我们需要的值。
useQuery 返回值
使用 useQuery 时,它会返回一些值,我们可以从钩子的返回值中解构这些值来做一些逻辑处理:
jsx
const values = useQuery(...);
const { data, error, status, fetchStatus }= useQuery(...);
主要有这几个:
- data
- error
- status
- fetchStatus
data
data 是 queryFn 返回的最后一次成功解析的数据:
jsx
const App = () => {
const { data } = useQuery({
queryKey: ["api"],
queryFn: fetchData,
});
return (
<div>
{data ? data.username : null}
</div>
);
};
error
通过 error 变量,可以访问查询函数失败后返回的错误对象。下面是使用 error 变量的方法:
jsx
const App = () => {
const { error } = useQuery({
queryKey: ["api"],
queryFn: fetchData,
});
return (
<div>
{error ? error.message : null}
</div>
);
};
status
在执行查询时,查询可能会经历几种状态。这些状态有助于向用户提供更多反馈。以下是 status 可能具有的状态:
loading
:尚未完成查询,也没有缓存数据。error
:执行查询时出错,error 属性会收到查询函数返回的错误信息。success
:查询成功,并已返回数据,data 属性会接收查询函数返回的成功数据。
使用方法:
jsx
const App = () => {
const { status, error, data } = useQuery({
queryKey: ["api"],
queryFn: fetchData,
});
if(status === "loading" ) {
return <div>Loading...</div>
}
if(status === "error" ) {
return <div>error: {error.message}</div>
}
return (
<div>
{data.hello}
</div>
);
};
为了方便起见,React Query 还引入了一些布尔值的变量来帮助我们识别每种状态。它们如下
isLoading
:status 处于 loading 状态isError
:status 处于 error 状态isSuccess
:status 处于 success 状态
让我们重写之前的代码:
jsx
const App = () => {
const { isLoading, isError, error, data } = useQuery({
queryKey: ["api"],
queryFn: fetchData,
});
if(isLoading) {
return <div>Loading...</div>
}
if(isError) {
return <div>error: {error.message}</div>
}
return (
<div>
{data.hello}
</div>
);
};
fetchStatus
在 React Query v3
中,发现在处理用户离线的场景时存在一个问题:如果用户触发了查询,但由于某种原因在请求过程中失去了连接,状态变量将一直处于加载状态,直到用户恢复连接并自动重试查询。
为了解决这类问题,React Query v4
引入了一个名为 networkMode
的新属性。该属性可以有三种状态,但默认情况下会使用 online 状态。这种模式可以使用 fetchStatus
变量。
fetchStatus
变量提供了查询函数的相关状态信息。以下是该变量可能具有的状态:
fetching
:当前正在执行查询函数。这意味着当前正在获取数据。paused
:查询想要获取数据,但由于连接中断,现在已停止执行。这表示当前已暂停。idle
:查询目前没有任何操作。这表示当前处于空闲状态。
可以这样使用:
jsx
const App = () => {
const { fetchStatus, data } = useQuery({
queryKey: ["api"],
queryFn: fetchData,
});
if(fetchStatus === "paused" ) {
return <div>Waiting for your connection to return...</div>
}
if(fetchStatus === "fetching" ) {
return <div>Fetching...</div>
}
return (
<div>
{data.hello}
</div>
);
};
与 status 一样,React Query 也引入了一些布尔值的变量来帮助识别其中的两种状态。它们如下
isFetching
:fetchStatus 变量处于获取状态isPaused
:fetchStatus 变量处于暂停状态
重写之前的代码:
jsx
const App = () => {
const { isFetching, isPaused, data } = useQuery({
queryKey" [""pi"],
queryFn: fetchData,
});
if(isPaused) {
return <div>Waiting for your connection to return...</div>
}
if(isFetching) {
return <div>Fetching...</div>
}
return (
<div>
{data.hello}
</div>
);
};
常用 options 说明
使用 useQuery 时,除了 queryKey
和 queryFn
外,还可以向其中传递更多 options
。这些options可帮助我们实现更好的开发体验和用户体验。主要有以下的几个:
- staleTime
- cacheTime
- retry
- retryDelay
- enabled
- onSuccess
- onError
staleTime
staleTime 是查询数据的过期时间(以毫秒为单位)。当设定的时间过去后,这个查询就会被认为过期了。
当查询是未过期的时候,我们将从缓存中直接抓取数据,而不会触发更新缓存的新请求。当查询被标记为过期时,数据仍会从缓存中提取,但会先触发查询的自动重新获取。
默认情况下,所有查询都使用设置为 0 的 staleTime,这意味着所有缓存数据默认都被视为过期数据。
配置 staleTime 选项:
jsx
useQuery({
// 查询数据在一分钟内都是最新的
staleTime: 60000,
});
cacheTime
cacheTime
是指缓存中不活跃的数据在内存中保留的时间(以毫秒为单位)。一旦超过这个时间,数据将被垃圾回收。
默认情况下,当查询没有 useQuery 的活动实例时,查询会被标记为不活跃的。此时,查询数据将在缓存中保留 5 分钟。5 分钟后,这些数据将被垃圾回收。
配置 cacheTime
选项:
jsx
useQuery({
// 在查询停止 1 分钟后,数据将被垃圾回收
cacheTime: 60000,
});
retry
retry 表示查询失败时是否重试。为 true 时,将重试直至成功。如果为 false,则不会重试。该属性也可以是一个数字。当它是一个数字时,查询将重试指定的次数。
默认情况下,所有失败的查询都会重试 3 次。
使用方法:
jsx
useQuery({
retry: false,
// 如果无法获取查询,那么它会重试 1 次
// retry: 1,
});
retryDelay
retryDelay 选项是下次重试前的延迟时间(单位为毫秒)。
默认情况下,React Query 使用指数回退延迟算法(exponential backoff delay algorithm)来定义两次重试之间的重试时间。
配置 retryDelay
选项:
jsx
useQuery({
retryDelay: (attempt) => attempt * 2000,
});
上面定义了一个线性延迟函数作为 retryDelay
选项。每次重试时,该函数都会接收尝试次数并乘以 2000
,意思就是每次重试的间隔时间将延长 2 秒。
enabled
enabled 表示何时可以执行查询。默认情况下,该值为 true,即查询都回立即执行。
我们可以这样使用 enabled 选项:
jsx
useQuery({
enabled: arrayVariable.length > 0
});
意思是只有在 arrayVariable
的大于 0 时才触发查询,否则不会执行查询。
onSuccess
onSuccess 选项是一个函数,当查询成功时会触发该函数。可以这样使用:
jsx
useQuery({
onSuccess: (data) => console.log("查询成功", data),
});
onError
onError 也是一个函数,会在查询失败时触发。可以这样使用:
jsx
useQuery({
onError: (error) => console.log("查询失败", error.message),
});
重新请求数据(Refetching)
有时候,你需要更新数据,因为可能数据已经过期,或者只是因为你有一段时间没有打开页面。无论是手动还是自动,React Query 都支持重新获取数据。
自动重请求
React Query 提供了几个 options,可以让你更轻松地保持服务端状态的新鲜度。为此,它会在某些情况下自动重新获取数据。
queryKey
queryKey
用于标识查询。在之前有提到,建议将所有查询函数的参数作为 queryKey
的一部分。为什么要这样做呢?
因为每当其中一些参数发生变化时,你的 queryKey
也会随之变化,而当你的 queryKey
发生变化时,你的查询也会自动重新执行。
让我们看看下面的例子:
jsx
const [someVariable, setSomeVariable] = useState(0)
useQuery({
queryKey: ["api", someVariable],
queryFn: fetchData,
});
return <button onClick={() => setSomeVariable(someVariable + 1)}>Click me</button>
我们定义了一个以 someVariable
作为 queryKey
的 useQuery。该查询将像往常一样在初始渲染时获取,但当我们点击按钮时,someVariable
的值将发生变化。queryKey
也会发生变化,这将触发查询自动重新请求,从而获得新数据。
Refetching options
在上面 "常用 options 说明"
那部分,实际上还有几个选项没说,它们是默认启用的,通常,只有在它们影响你的业务逻辑的情况下关闭,一般最好还是保持开启状态。
以下是 useQuery 默认启用的与数据重新获取相关的选项:
refetchOnWindowFocus
:每当你聚焦到当前窗口时,就会触发一次重新获取。例如,如果你返回应用程序时切换了标签页,React Query 就会触发数据重新获取。refetchOnMount
:每当新组件挂载时,此选项都会触发一次重新获取。例如,当一个使用你定义的钩子的新组件挂载时,React Query 就会触发数据的重新获取。refetchOnReconnect
:当失去网络连接时,就会触发一次重新获取。
需要注意的一点是 ,这些选项默认只会在数据被标记为过期时重新获取数据。即使是过期数据也可以重新获取,因为所有这些选项(布尔值除外)都支持接收一个值为 "always " 的字符串。当这些选项的值为 "always" 时,即使数据没有过期,也会始终触发重新获取。
配置如下:
jsx
useQuery({
refetchOnMount: "always",
refetchOnReconnect: true,
refetchOnWindowFocus: false
});
在上面的代码中:
- 对于
refetchOnMount
,我们总是希望钩子在任何使用它的组件挂载时重新获取数据,即使缓存的数据不是过期的也重新获取 - 对于
refetchOnReconnect
,我们希望钩子在离线后重新获得连接时重新获取数据,但仅限于数据过期的情况。 - 对于
refetchOnWindowFocus
,我们不希望钩子在窗口聚焦时重新获取数据
可能会想到一个问题:是否有办法强制每隔几秒钟重新获取一次数据,即使数据并没有过期。即使你没有想到,React Query 也帮你做了。React Query 添加了另一个与重新获取相关的选项,叫做 refetchInterval
。该选项允许你以毫秒为单位指定查询重新获取数据的频率。
我们可以这样使用它:
jsx
useQuery({
refetchInterval: 2000,
refetchIntervalInBackground: true
});
在上面的代码中,我们将钩子配置为始终每 2 秒重新获取一次。我们还添加了另一个名为 refetchIntervalInBackground
的选项,其值为 true。即使窗口或标签页在后台运行,该选项也会允许查询继续重新获取。这样,自动重新获取就完成了。
下面,让我们看看如何在代码中触发手动重新获取。
手动重请求
手动触发查询重取有两种方法。可以使用 QueryClient 或从钩子中获取 refetch 函数。
使用 QueryClient
可以利用 QueryClient 在需要时强制重新获取数据。可以这样做:
jsx
const queryClient = useQueryClient();
queryClient.refetchQueries({ queryKey: ["api"] })
通过使用 QueryClient,我们调用了它提供的一个 refetchQueries
的函数。通过该函数,可以触发重新获取与给定 queryKey
匹配的所有查询。在本代码中,我们将触发对所有具有["api"]
queryKey
的查询。
使用 refetch
useQuery 的返回值提供了一个 refetch 函数。通过该函数,可以触发对该查询的重新获取。可以这样做:
jsx
const { refetch } = useQuery({
queryKey: ["api"],
queryFn: fetchData,
});
// 使用
refetch()
从 useQuery 返回值中解构 refetch
函数。然后,只要我们想强制重新获取查询,就可以调用该函数。
依赖查询
在开发过程中,有时一个查询的执行依赖于之前的一个查询。在这种情况下,我们需要使用所谓的依赖查询。通过 enabled
选项,React Query
可以让查询依赖于其他查询。
你可以这样做:
jsx
const App = () => {
const { data: firstQueryData } = useQuery({
queryKey: ["api"],
queryFn: fetchData,
});
const canThisDependentQueryFetch = firstQueryData?.hello !== undefined;
const { data: dependentData } = useQuery({
queryKey: ["dependentApi", firstQueryData?.hello],
queryFn: fetchDependentData,
enabled: canThisDependentQueryFetch,
});
// ...
}
页面:
解释一下:
- 创建一个以
["api"]
作为queryKey
、以fetchData
函数作为查询函数的查询。 - 创建一个
canThisDependentQueryFetch
的变量,用于检查之前的查询是否包含我们需要的数据。这个布尔变量将帮助我们决定下一个查询是否可以获取数据。 - 然后以
["dependentAPI", firstQueryData?.hello]
作为queryKey
,以fetchDependentData
函数作为queryFn
,以canThisDependentQueryFetch
作为enabled
,创建第二个查询。 - 当上一个查询完成数据获取后,
canThisDependentQueryFetch
将被置为 true,并执行第二个查询。
try 一 try
先准备一个接口请求的方法:
jsx
export const fetchData = async ({ queryKey }) => {
const { apiName } = queryKey[0];
const response = await fetch(
`https://danieljcafonso.builtwithdark.com/${apiName}`
);
if (!response.ok) throw new Error("Something failed in your request");
return response.json();
};
export const apiA = "react-query-api";
export const apiB = "react-query-api-two";
然后定义三个组件。
第一个组件 ComponentA
:
jsx
import { useQuery } from "@tanstack/react-query";
import { fetchData, apiA } from "../api";
import ComponentB from "./ComponentB";
const ComponentA = () => {
const { data, error, isLoading, isError, isFetching } = useQuery({
queryKey: [{ queryIdentifier: "api", apiName: apiA }],
queryFn: fetchData,
retry: 1,
});
if (isLoading) return <div> Loading data... </div>;
if (isError) return <div>error: {error.message}</div>;
return (
<div>
<p>ComponentA: {isFetching ? "Fetching Component A..." : data.hello}</p>
<ComponentB />
</div>
);
};
export default ComponentA;
组件 ComponentB
:
jsx
import { useQuery } from "@tanstack/react-query";
import { fetchData, apiB } from "../api";
import ComponentC from "./ComponentC";
const ComponentB = () => {
const { data } = useQuery({
queryKey: [{ queryIdentifier: "api", apiName: apiB }],
queryFn: fetchData,
onSuccess: (data) => console.log("Component B fetched data", data),
});
return (
<div>
<span>ComponentB: {data?.hello}</span>
<ComponentC parentData={data} />
</div>
);
};
export default ComponentB;
组件 ComponentC
:
jsx
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { fetchData, apiA } from "../api";
const ComponentC = ({ parentData }) => {
const { data, isFetching } = useQuery({
queryKey: [{ queryIdentifier: "api", apiName: apiA }],
queryFn: fetchData,
enabled: parentData !== undefined,
});
const queryClient = useQueryClient();
const handleOnClick = () => {
queryClient.refetchQueries({
queryKey: [{ queryIdentifier: "api", apiName: apiA }],
});
};
return (
<div>
<p>ComponentC: {isFetching ? "Fetching Component C..." : data.hello} </p>
<button onClick={handleOnClick}>Refetch Parent Data</button>
</div>
);
};
export default ComponentC;
大致梳理一下逻辑:
-
当组件 A 渲染时:
- 使用
[{ queryIdentifier: "api", apiName: apiA }]
queryKey 的 useQuery 实例挂载:- 由于这是第一次挂载,没有缓存也没有之前的请求,因此会立即请求接口数据,并且查询函数将接收
queryKey
作为QueryFunctionContext
的一部分。 - 数据获取成功后,数据将缓存在
[{ queryIdentifier: "api", apiName: apiA }]
的queryKey
下。 - 由于我们假设默认的 staleTime 为 0,因此钩子会将其数据标记为过期数据。
- 由于这是第一次挂载,没有缓存也没有之前的请求,因此会立即请求接口数据,并且查询函数将接收
- 使用
-
当组件 A 渲染组件 B 时:
- 一个带有
[{ queryIdentifier: "api", apiName: apiB }]
queryKey 的 useQuery 实例被挂载:- 由于这是第一次挂载,既没有缓存也没有之前的请求,因此会立即请求数据。
- 数据获取成功后,数据将缓存在
[{ queryIdentifier: "api", apiName: apiB }]
queryKey
下,钩子将调用 onSuccess 函数。 - 由于我们假设默认的 staleTime 为 0,因此钩子会将其数据标记为过期数据。
- 一个带有
-
当组件 B 渲染组件 C 时:
- 带有
[{ queryIdentifier: "api", apiName: apiA }]
queryKey 的 useQuery 实例挂载:- 由于该钩子与组件 A 中的钩子具有相同的
queryKey
,钩子下已经有缓存数据,因此可以立即访问数据。 - 由于该查询在上次取回后被标记为过期,因此该钩子需要重新取回,但它需要先等待该查询的
enabled
条件变为 true。 - 一旦
enabled
为 true,查询就会触发重新获取。这使得组件 A 和组件 C 上的isFetching
都为 true。 - 一旦获取请求成功,数据将被缓存在
[{ queryIdentifier: "api", apiName: apiA }]
查询键下,并且查询再次被标记为过期。
- 由于该钩子与组件 A 中的钩子具有相同的
- 带有
-
再来看一下组件 A 卸载的情况:
-
由于不再有任何使用
[{ queryIdentifier: "api", apiName: apiA }]
queryKey 的查询实例处于活跃状态,默认的缓存超时时间为 5 分钟,5 分钟后,该查询下的数据就会被删除并被垃圾回收。 -
由于不再有任何使用
[{ queryIdentifier: "api", apiName: apiB }]
queryKey 的查询实例处于活跃状态,默认缓存超时时间为 5 分钟,5 分钟后,该查询下的数据就会被删除并被垃圾回收。
-
如果你能在查询使用过程中跟踪前面的过程和查询的生命周期,那么恭喜你:你已经基本解了 useQuery 的工作原理!