React Query is a powerful library for fetching, caching, and updating asynchronous data in React applications. One of its key strengths is its built-in caching mechanism, which dramatically improves performance and user experience by reducing unnecessary network requests. This blog post will dive deep into how React Query's caching works and how to leverage it effectively.
Understanding React Query's Cache
At its core, React Query stores fetched data in an in-memory cache. This cache is organized using a unique query key for each query. When you make a request using useQuery, React Query first checks if data with the specified query key already exists in the cache. If it does and the data is considered fresh, React Query returns the cached data without making a new network request.
Key Concepts:
- Query Key: A unique identifier for your query. It's typically an array of values, allowing you to create dynamic and specific keys (e.g.,
['posts', { userId: 1 }]). - Stale Time: The duration after which cached data is considered stale. Even if the data is available in the cache, React Query will refetch it in the background if the stale time has passed. The default is 0 seconds.
- Cache Time: The duration for which inactive cache data remains in memory. After this time, the data is garbage collected to free up resources. The default is 5 minutes.
How Caching Works in Practice
Let's illustrate with a simple example:
import { useQuery } from 'react-query';
const fetchPost = async (postId) => {
const response = await fetch(`/api/posts/${postId}`);
if (!response.ok) {
throw new Error('Failed to fetch post');
}
return response.json();
};
function Post({ postId }) {
const { data, isLoading, error } =
useQuery(['post', postId], () => fetchPost(postId));
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h2>{data.title}</h2>
<p>{data.content}</p>
</div>
);
}
In this example:
- We're using
useQueryto fetch a post based on thepostIdprop. - The query key is
['post', postId], which ensures that each post has a unique cache entry. - The
fetchPostfunction handles the actual data fetching.
Here's what happens when the component renders:
- First Render: If no data exists in the cache for the query key
['post', postId], React Query makes a network request usingfetchPost(postId). The data is then stored in the cache. - Subsequent Renders (within stale time): If the component re-renders (e.g., due to a prop change), and the data in the cache is still considered fresh (i.e., the
staleTimehasn't passed), React Query immediately returns the cached data. No network request is made. - After Stale Time: If the component re-renders and the
staleTimehas passed, React Query returns the cached data immediately (providing a fast initial render) and then makes a network request in the background to update the cache. The component will re-render with the new data when the background fetch completes. This provides a smooth user experience as the UI is always responsive. - Garbage Collection: If the
Postcomponent is unmounted and the cache entry['post', postId]remains unused for the duration of thecacheTime, React Query will remove the entry from the cache to conserve memory.
Customizing Cache Behavior
React Query provides several options to customize its caching behavior:
staleTime: Configure how long data remains fresh. For example,staleTime: 60 * 1000(1 minute) means data will be considered fresh for one minute.cacheTime: Configure how long inactive data remains in the cache. For example,cacheTime: 300 * 1000(5 minutes) is the default. Set toInfinityto keep data in the cache indefinitely.refetchOnMount: Controls whether React Query refetches data when the component mounts. Defaults totrue.refetchOnWindowFocus: Controls whether React Query refetches data when the browser window gains focus. Defaults totrue.refetchOnReconnect: Controls whether React Query refetches data when the browser reconnects to the internet. Defaults totrue.retry: Controls how many times React Query retries failed requests.
You can configure these options either globally using the QueryClient or on a per-query basis:
import { useQuery, QueryClient, QueryClientProvider } from 'react-query';
// Global configuration
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minute
cacheTime: 300 * 1000, // 5 minutes
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyComponent />
</QueryClientProvider>
);
}
function MyComponent() {
// Per-query configuration
const { data, isLoading, error } = useQuery('todos', fetchTodos, {
staleTime: Infinity, // Data never becomes stale
refetchOnWindowFocus: false, // Disable refetching on window focus
});
// ...
}
Invalidating and Refetching Queries
Sometimes, you need to manually invalidate and refetch queries to ensure the cache is up-to-date. React Query provides the useQueryClient hook for this:
import { useQueryClient } from 'react-query';
function MyComponent() {
const queryClient = useQueryClient();
const handleUpdate = async () => {
// Perform the update (e.g., using an API call)
await updateData();
// Invalidate the 'todos' query, forcing a refetch
queryClient.invalidateQueries('todos');
};
// ...
}
queryClient.invalidateQueries('todos') marks the 'todos' query as stale, causing React Query to refetch it in the background.
Conclusion
React Query's caching mechanism is a powerful tool for optimizing your React applications. By understanding how query keys, staleTime, and cacheTime work, you can effectively manage your data and provide a fast and responsive user experience. Remember to use invalidateQueries when you need to manually update the cache after data mutations. Experiment with different configurations to find the sweet spot for your specific application needs.