Tanstack (React) Query - Caching
Tanstack (React) Query is one of my favourite libraries and for a good reason. It makes client-side fetching incredibly easy. It also offers a ton of useful features out of the box like polling, refetching, pagination and much, much more. One of the most underrated features has to be caching though.
What is caching?
In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere.
When working with any cache there are two options:
Tanstack Query uses an in-memory cache which is used to store and manage asynchronous data in your app. In essence, when a network request is made the first thing that happens is a cache check. If the requested data is in the cache it gets returned from the cache with no delays. If the requested data isn't in the cache then a network request is made so the data is retrieved and then it's added to the cache.
How does Tanstack Query know what to look for in the cache though?
Importance of Unique Keys
Tanstack Query uses keys to identify and retrieve data from the cache. Let's see a bad example of a key:
const BLOG_POST = "Blog Post"
const getBlogPost = async (id: string) => {
// Fetching logic
}
const useGetBlogPost = (id: string) => {
return useQuery(
[BLOG_POST],
() => getBlogPost(id)
)
}
Now when we call the useGetBlogPost hook with an id of 1 the response that we get back will be saved in the cache under the key ["Blog Post"]. The cache will look something like this:
{
["Blog Post"]: {
id: "1",
title: "First post"
// ...rest of the properties
}
}
But what happens if we call the useGetBlogPost hook with an id of 2 after we've called it with an id of 1? Let's go through the process that Tanstack Query goes through when a network request occurs. It will first check the cache. Is there a value associated with the key with which we're calling the useQuery hook? In our case, when calling useGetBlogPost the key will always be ["Blog Post"] regardless of the id that we send. This means that when we call the hook for the second time we'll get a cache hit and we'll get served the first post from the cache. This isn't the correct behaviour.
So how can we fix this? We need to make sure that each key is unique. We can simply include the id of the post in the key and that will solve the problem:
const useGetBlogPost = (id: string) => {
return useQuery(
[BLOG_POST, id],
() => getBlogPost(id)
)
}
Now when we call the useGetBlogPost hook with an id of 1 our cache will look like so:
{
["Blog Post", "1"]: {
id: "1",
title: "First post"
// ...rest of the properties
}
}
Then if we call the useGetBlogPost hook with an id of 2 we are no longer going to get a cache hit but rather we'll get a cache miss and the cache will be updated to:
{
["Blog Post", "1"]: {
id: "1",
title: 'First post'
// ...rest of the properties
},
["Blog Post", "2"]: {
id: "2",
title: "Second post"
// ...rest of the properties
}
}
领英推荐
What if data in the cache becomes "stale"?
In Tanstack Query, data is considered "stale" when it might be outdated and needs to be refetched. When you fetch data using the useQuery hook, the data is stored in the cache and marked as "fresh". After a certain period of time (which is configurable, more on this later), the data becomes "stale", but it's still kept in the cache.
How can we ensure that we always have fresh data?
const UpdatePost = ({ id }: { id: string }) => {
const { refetch } = useGetBlogPost(id)
return (
<button onClick={() => refetch()}>Update post</button>
)
}
const updateBlogPost = async (id: string, post: Partial<Post>) => {
// Update logic
}
const useUpdateBlogPost = (id: string, post: Partial<Post>) => {
return useMutation(
() => updateBlogPost(id, post), {
onSuccess: () => {
queryClient.invalidateQueries([BLOG_POST, id]);
},
})
}
When should I invalidate the cache?
Whether you can tolerate stale data and whether you need fresh data will depend on your individual use case.
For apps that rely on data that rarely changes, stale data isn't that big of a deal especially if all possible data changes are within your user's control. If your app fits the description then setting the refetchOnWindowFocus property to false might be a good idea especially if you are using a Serverless architecture and you get billed for each network request made by your app. Also setting refetchInterval doesn't make much sense.
On the other hand, if there is a high chance that at any single moment the data in the cache can become outdated then you need to make sure that you're very careful when grabbing data from the cache. Make sure you use all the discussed tools above to ensure that your data is fresh.
More background cache updates?
As already discussed, by default Tanstack Query refetches data (and updates the cache) in the background if the window gets refocused, or when a network connection is reestablished. There is another case in which a background refetch can happen and that could cause some confusion.
By default even if you get a cache hit, Tanstack Query will do a refetch in the background and update the cache. This is because by default Tanstack Query considers all data in the cache stale. This could be exactly what you want if you want your data to be as fresh as possible. If that isn't want you want though then you'll need to make an adjustment. The property staleTime determines how long until data stored in the cache becomes stale. If you don't want any background refetches upon a cache hit then you can set the value of staleTime to Infinity.
Combination of caching behaviours
You don't have to go with just one type of caching behaviour for your entire app. You can have a combination of queries that can tolerate stale data and queries that need fresh data at all times.
You can configure your QueryClient in any way that makes sense to you. For example you might have an app that can tolerate mostly stale data so it makes sense to configure your QueryClient in the following way:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: Infinity
},
}
});
But you might have a query which cannot tolerate stale data so you can overwrite the default behaviour for that particular query:
const COMMENTS = 'Comments'
const getComments = async (postId: string) => {
// fetching logic
}
const useGetComments = (postId: string) => {
return useQuery(
[COMMENTS, postId],
() => getComments(postId),
{
staleTime: 0,
refetchOnWindowFocus: true,
refetchInterval: 30_000 // 30 seconds
}
)
}
As you can see you don't have to stick with just one caching behaviour in your app. You can use a combination of different behaviours.