2. Expiration Time and Cache Strategy for useQuery
Expiration Time
Developers can estimate the approximate expiration time of data in their specific business scenarios, for example:
Fund return details – the return details of 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.
Managing expiration times manually 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 lifetime. 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:
The
queryKeychanges (for example, 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 implement conditional requests by passing the enabled parameter. 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, 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 whether 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
},
}
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
},
}