2. useQuery 的过期时间与缓存策略 (English version below)
过期时间
开发者能够预估出当前业务场景下数据大致的过期时间,比如
基金收益明细,每只基金的收益明细每天只会更新一次。
用户头像,用户头像只有在用户自己手动修改之后才会变更。
评论列表,评论列表如果实时性要求不高,半小时更新一次也能接受。
如果我们手动去管理过期时间,一定是一个非常痛苦的过程。好在 useQuery 提供了 staleTime 参数用来定义过期时间(毫秒单位)。
如果没超时,将不会发起请求,而是直接返回缓存数据。这在一定程度上减少了网络请求,减轻服务器压力同时提升了用户体验。
例如:
{
staleTime: 60000
}
意味着一分钟内不会再发起网络请求,而是直接从缓存里取值返回。
该值默认为 0,缓存有效期应由开发人员和业务场景共同决定。我建议在设置缓存有效期时与产品经理、后端开发、测试同学沟通,确定一个合理的缓存有效期。不要通过拍脑袋决定缓存有效期,否则很容易给后续的维护带来麻烦。
注意:即便缓存过期了,query 也不会自动重新获取数据。什么时候才会触发 query 的自动更新呢?
Trigger
只有在以下四种情况下才会更新缓存数据。
queryKey变更(比如 queryKey 是[todo, page],当page变更时,意味着用户想看下一页数据了,query 会重新请求数据)订阅过期状态的组件渲染到页面(比如一个引用了过期缓存的弹窗,该弹窗的弹出会导致更新缓存)
浏览器窗口重新获取焦点
设备网络成功重连
其中三种情况可以手动关闭
useQuery({
queryKey: ['todo', sort],
queryFn: () => fetchTodo(sort),
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
// 也可以设置无穷大 Infinity
staleTime: 30 * 1000,
})
条件性请求
所谓条件性请求,就是指 query 只在特定场景或条件满足时才会执行。
比如:
//!!! 错误示范
if (keyword) {
useQuery({
queryKey: ['search', keyword],
queryFn,
})
}
但很明显,这违反了 hook 的设计规范。
可以通过传入 enabled 参数来实现条件性请求。即:仅当 enabled 为 true 时,query 才会被触发。
useQuery({
queryKey: ['search', keyword],
queryFn,
enabled: !!keyword,
})
依赖性请求
依赖性请求是条件性请求的一种,指当前请求依赖于其他请求的结果。
比如:一本书的评论需要等待书籍详情加载完成后再加载。
//!!! 耦合度过高
useQuery({
queryKey: ['book', 'comments', bookId],
queryFn: async () => {
const book = await fetchBookDetail(bookId)
const author = await fetchAuthorDetail(book.data.authorId)
return {
book,
author,
}
},
})
依赖性请求可以在 queryFn 里通过多个 await 处理,但这并不利于逻辑的抽象。
我们把网络请求封装为 hook 就是为了把逻辑抽象出来,封装好,以备后续复用。
如果两个请求不是必然关联在一起,我更推荐拆分开,基于 enabled 控制请求触发时机。这样更灵活,耦合度更低,是更优雅的逻辑抽象。
const useBookDetail = (bookId: string) => {
return useQuery({
queryKey: ['book', bookId],
queryFn: () => fetchBookDetail(bookId),
enabled: !!bookId,
})
}
const useBookComments = (bookId: string, enabled: boolean) => {
return useQuery({
queryKey: ['bookComments', bookId],
queryFn: () => fetchBookComments(bookId),
enabled,
})
}
const useBookDetailAndComments = (bookId: string) => {
const { isSuccess, data: book } = useBookDetail(bookId)
const comments = useBookComments(bookId, isSuccess)
return {
comments,
book,
}
}
轮询
轮询通常用于那些需要实时性反馈的场景,比如查询用户是否完成支付。
useQuery 的 refetchInterval 参数可以定义轮询时间,单位毫秒。
useQuery({
queryKey: ['list', { sort }],
queryFn,
refetchInterval: 5000, // 5 seconds 轮询
})
refetchInterval 也可以是一个函数。
示例:前端通过轮询来得知用户是否完成支付,用户一旦完成支付轮询就该停止。
{
...,
refetchInterval: (query) => {
// query 是当前请求的 query 对象,包含了本次query的各种配置和元信息。
// 数据结构由react-query提供
// query.state.data 是当前请求的响应数据
if (query.state.data?.finished) {
return false
}
return 3000
},
}
English
【@tanstack/query】Expiration Time and Cache Strategy for useQuery
Expiration Time
Developers can estimate the approximate expiration time of data in their business scenario, for example:
Fund return details – the return details for each fund are updated only once a day.
User avatar – the avatar only changes when the user manually modifies it.
Comment list – if real-time requirements are low, updating every half hour is acceptable.
Manually managing expiration times would certainly be a very painful process. Fortunately, useQuery provides the staleTime parameter to define the expiration time (in milliseconds).
If the data has not expired, no request will be made and cached data is directly returned. This reduces network requests to some extent, eases server load, and improves user experience.
For example:
{
staleTime: 60000
}
This means that within one minute, no network request will be made; the value is directly returned from the cache.
The default value is 0. The cache shelf life should be determined by the developer and the business scenario. I suggest discussing with the product manager, backend developers, and QA to agree on a reasonable cache duration. Do not decide the cache expiration by intuition, otherwise it will likely cause trouble for future maintenance.
Note: Even if the cache expires, the query will not automatically refetch data. So when does a query trigger an automatic update?
Trigger
Only the following four situations will update cached data:
queryKeychanges (e.g., if the queryKey is[todo, page]andpagechanges, it means the user wants to see the next page’s data, and the query will re‑request data)A component that uses expired state mounts (e.g., a popup referencing an expired cache; when the popup appears, the cache will be updated)
The browser window regains focus
The device successfully reconnects to the network
Three of these can be disabled manually:
useQuery({
queryKey: ['todo', sort],
queryFn: () => fetchTodo(sort),
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
// can also be set to Infinity
staleTime: 30 * 1000,
})
Conditional Requests
Conditional requests mean that a query is only executed under a specific scenario or when a certain condition is met.
For example:
//!!! Wrong example
if (keyword) {
useQuery({
queryKey: ['search', keyword],
queryFn,
})
}
But obviously, this violates the design rules of hook.
You can solve conditional requests by passing enabled. That is, the query will only be triggered when enabled is true.
useQuery({
queryKey: ['search', keyword],
queryFn,
enabled: !!keyword,
})
Dependent Requests
Dependent requests are a special type of conditional request, meaning the current request depends on the result of another request.
For example: suppose comments for a book should load only after the book details have finished loading.
//!!! Too tightly coupled
useQuery({
queryKey: ['book', 'comments', bookId],
queryFn: async () => {
const book = await fetchBookDetail(bookId)
const author = await fetchAuthorDetail(book.data.authorId)
return {
book,
author,
}
},
})
Dependent requests can be handled with multiple await statements inside queryFn, but this is not conducive to logic abstraction.
We wrap network requests as hooks precisely to abstract the logic and encapsulate it for later reuse.
If two requests are not necessarily tied together, I recommend splitting them and controlling the trigger timing based on enabled. This is more flexible, has lower coupling, and is a more elegant logical abstraction.
const useBookDetail = (bookId: string) => {
return useQuery({
queryKey: ['book', bookId],
queryFn: () => fetchBookDetail(bookId),
enabled: !!bookId,
})
}
const useBookComments = (bookId: string, enabled: boolean) => {
return useQuery({
queryKey: ['bookComments', bookId],
queryFn: () => fetchBookComments(bookId),
enabled,
})
}
const useBookDetailAndComments = (bookId: string) => {
const { isSuccess, data: book } = useBookDetail(bookId)
const comments = useBookComments(bookId, isSuccess)
return {
comments,
book,
}
}
Polling
Polling is often used in scenarios that require real‑time feedback, such as checking if a user has completed payment.
The refetchInterval parameter of useQuery defines the polling interval (in milliseconds).
useQuery({
queryKey: ['list', { sort }],
queryFn,
refetchInterval: 5000, // poll every 5 seconds
})
refetchInterval can also be a function.
Example: The frontend polls to know whether the user has completed payment; once the payment is finished, polling should stop.
{
...,
refetchInterval: (query) => {
// query is the current query object, containing various configurations and metadata for this query.
// The data structure is provided by react-query.
// query.state.data is the response data of the current request
if (query.state.data?.finished) {
return false
}
return 3000
},
}