Wanna see something cool? Check out Angular Spotify 🎧

Understanding staleTime and gcTime (cacheTime) in React Query

In React Query, two key options control the freshness and caching duration of query results: staleTime and gcTime (formerly cacheTime).

Here’s a brief explanation of each:

  • staleTime: The duration a query result is considered fresh. While fresh, data is read from the cache only, with no network requests. Once stale (default is instantly), data is still read from the cache, but a background refetch can happen under certain conditions.
  • gcTime: The duration inactive queries remain in the cache before being removed. This defaults to 5 minutes. Queries become inactive when no components are observing them.

Typically, you may need to adjust staleTime more often than gcTime.

staleTime

To understand staleTime, consider a simple example of a hook to fetch a single vehicle:

export const useFetchVehicle = (vehicleId: string | undefined) => {
  const [vehicle, setVehicle] = useState<Vehicle | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  useEffect(() => {
    const init = async () => {
      try {
        const data: Vehicle = await fetchVehicle(vehicleId);
        setVehicle(data);
      } catch (err) {
        setError((err as Error).message);
      } finally {
        setLoading(false);
      }
    };
    init();
  }, [vehicleId]);
  return { vehicle, loading, error };
};

Using React Query, we can simplify this with the useQuery hook, eliminating the need for useState and useEffect:

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

export const useFetchVehicle = (vehicleId: string | undefined) => {
  return useQuery({
    queryKey: ['vehicle', vehicleId],
    queryFn: () => fetchVehicle(vehicleId),
  });
};

Default staleTime of 0

In the above code, useQuery manages the loading, error, and data states. The queryKey uniquely identifies the query, and queryFn fetches the data. Since staleTime is not set, it defaults to 0.

Notice how selecting the first item triggers an API call for /vehicle/1. Returning to the list and selecting the first item again triggers another API call. The UI renders immediately with cached data, but a background refetch happens due to the default staleTime of 0.

staleTime

In DevTools, ['vehicle', '1'] is considered stale immediately with staleTime set to 0.

staleTime

A background refetch can happen when:

  • New instances of the query mount
  • The window is refocused
  • The network is reconnected
  • The query is configured with a refetch interval

Read more from Important Defaults.

Setting staleTime to another value

Consider setting staleTime to 30 seconds (30 * 1000 milliseconds):

export const useFetchVehicle = (vehicleId: string | undefined) => {
  return useQuery({
    queryKey: ['vehicle', vehicleId],
    queryFn: () => fetchVehicle(vehicleId),
+   staleTime: 30 * 1000,
  });
};

Now, selecting the first item triggers an API call for /vehicle/1. Returning to the list and selecting the first item again does not trigger an API call. The UI renders immediately with cached data, considered fresh for 30 seconds, with no background refetch.

useQuery not zero

In DevTools, ['vehicle', '1'] is considered fresh for 30 seconds with staleTime set to 30 seconds.

DevTools Fresh Data

Why the default staleTime is 0

Quoted from UI Dev Data Synchronization.

The default staleTime of 0 means every query is instantly considered stale. This “aggressive but sane default” ensures frequent refetching, which is generally safer than fetching too infrequently.

The docs define this as “aggressive but sane defaults”.

Aggressive, because it means we might be refetching from the server more often than we need to, but sane because fetching too often is the lesser evil of the two options.

It’s a bit like re-renders in React. Yes, we all want to minimize our application’s re-renders, but having too many is significantly better than having too little where your view could be out of sync with your application’s state.

Also, if the default value weren’t 0, what would be a better default? 20 seconds? 30? 1 minute? It’s one of those cases that you can’t reliably set up for every possible situation. The answer is always it depends.

Specifically, it depends on the resource in question: How often is it updated? How accurate does the data displayed on your screen need to be? How collaborative is the environment you’re working in?

The answer to these questions should be decided by developers on a case by case basis.

If we fetch a Twitter post with all its likes and comments, it’s likely stale pretty fast. On the other hand, if we fetch exchange rates that update on a daily basis, well, our data is going to be quite accurate for some time even without refetching.

cacheTime (gcTime)

cacheTime does nothing while a query is in use. It activates once the query becomes unused, removing data from the cache after the specified duration (default is 5 minutes).

The time in milliseconds that unused/inactive cache data remains in memory. When a query’s cache becomes unused or inactive, that cache data will be garbage collected after this duration. When different garbage collection times are specified, the longest one will be used.

  • Defaults to 5 * 60 * 1000 (5 minutes) or Infinity during SSR.
  • Note: the maximum allowed time is about 24 days. See more at useQuery.
  • If set to Infinity, garbage collection is disabled.
  • gc refers to “garbage collect” time, a common term in computer science.

For example, the ['vehicle', '1'] query is removed from the cache after 5 minutes of inactivity.

In React Query v5, the cacheTime option has been renamed to gcTime.

Reference

Creating a fast feeling web app

I recently gave a 30-minute version of my talk, Creating Fast-Feeling Web Apps, at JSConfJP in Tokyo, one of the largest web developer events in Japan. The feedback from attendees was fantastic. During the talk, I covered topics like staleTime in React Query and how to use initialData to deliver an instant user experience. You can check out the slides and the recorded talk for more details

Published 24 Dec 2024

    Read more

     — Chrome DevTools Performance Panel: Analyze Your Website's Performance
     — Configuring Cloudflare Images and fixing ERROR 9421: Too many redirects
     — Sharing my go-to Gmail filter to clean up unnecessary Calendar notifications
     — TypeScript is Operator for Type Narrowing
     — nvm keeps "forgetting" node version in new VSCode terminal sessions