TanStack Query’s cache is less a passive storage and more an active participant in your UI’s state management, deciding what data to show, when to fetch it, and how to keep it fresh.

Let’s see it in action. Imagine you have a list of users.

import { useQuery } from '@tanstack/react-query';

function UsersList() {
  const { data: users, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      console.log('Fetching users...');
      const response = await fetch('/api/users');
      return response.json();
    },
  });

  if (isLoading) return <div>Loading users...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

When UsersList mounts, useQuery checks its internal cache for an entry with the queryKey: ['users']. If it’s not there, it initiates a fetch via queryFn. The fetched data is then stored in the cache associated with ['users'].

Now, if you navigate away from this component and then come back, useQuery again checks the cache for ['users']. This time, it finds the data. Instead of refetching immediately, it returns the cached data instantly, making your UI feel incredibly fast. While it’s showing you the cached data, it also marks the query as "stale."

This "staleness" is the key to automatic refetching. TanStack Query has several triggers that will cause a stale query to refetch:

  1. Window Focus: When the user clicks back into the browser tab.
  2. Reconnection: When the network connection is restored after being lost.
  3. Mounting: When a component using the same queryKey mounts after the initial fetch.
  4. Interval: If you configure refetchInterval (e.g., refetchInterval: 5000 for every 5 seconds).

Let’s add a UserDetail component that also fetches a single user by ID.

import { useQuery } from '@tanstack/react-query';

function UserDetail({ userId }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId], // Unique key for each user
    queryFn: async () => {
      console.log(`Fetching user ${userId}...`);
      const response = await fetch(`/api/users/${userId}`);
      return response.json();
    },
  });

  if (isLoading) return <div>Loading user...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
    </div>
  );
}

Notice the queryKey: ['user', userId]. This is crucial. ['users'] and ['user', 123] are distinct keys. If UsersList fetches users, it does not invalidate the cache for ['user', 123]. They are independent.

The QueryClient is the central manager. When you call queryClient.invalidateQueries(['users']), it tells the QueryClient to mark all queries whose keys start with ['users'] as stale and, if they are currently active, refetch them. This is how you manually trigger updates. For instance, after successfully updating a user’s profile, you’d invalidate the ['user', userId] query to ensure the detail view shows the latest data.

The "stale time" and "cache time" are vital configurations. staleTime is the duration after which a query’s data is considered stale. During this time, if the component re-mounts, it will use the cached data without refetching. Once staleTime passes, the data becomes stale, and subsequent mounts or background refetch triggers will initiate a fetch. cacheTime is the duration for which inactive query data is kept in memory. If a query becomes inactive (no components are listening to it) and its cacheTime passes, its data is garbage collected. The default staleTime is 0, meaning data is stale immediately after being fetched, and the default cacheTime is 5 minutes.

A common pattern is to set a staleTime greater than 0, say staleTime: 1000 * 60 * 5 (5 minutes). This means that for 5 minutes after the initial fetch, TanStack Query will not refetch the data on mount or window focus, significantly reducing unnecessary network requests while still allowing for manual invalidation when data definitely changes.

The QueryClient’s setQueryData method is a powerful tool for optimistic updates. You can directly write new data into the cache before the actual API call completes. If the API call succeeds, great. If it fails, you can easily revert the cache back to its previous state. This provides an instant UI update for the user, making your application feel much more responsive.

When you have multiple components listening to the same query key, TanStack Query efficiently manages this. The data is fetched only once. All components receive the same data from the cache. When a refetch occurs, all listening components are updated simultaneously. This de-duplication is a core benefit.

The queryFn is not just for fetching. It can also be used for mutations where you might return the updated data directly, bypassing a separate refetch call in some scenarios. This is powerful for complex data transformations or when the server response is the definitive new state.

Next, you’ll want to explore mutations, specifically how they interact with the cache and the various strategies for invalidating or updating cached data after a mutation.

Want structured learning?

Take the full React course →