TanStack Query (React Query): Handling HTTP Requests with EaseEntry 3 of 10

How Caching Works: staleTime and gcTime

TanStack Query stores every query result in the QueryClient cache under its key, shows cached data instantly, and refetches in the background when the data is stale.

Notes

Stale-while-revalidate flow:

  1. A component calls useQuery with ['events'].
  2. If the cache has data for that key, it's shown immediately.
  3. If that data is stale, queryFn runs again in the background.
  4. If the new response differs, the cache updates and the UI re-renders.

staleTime (default 0): how long data counts as fresh.

  • While fresh, cached data is reused with no refetch.
  • Once stale, the next trigger (component mount, window refocus, reconnect) refetches in the background.
  • staleTime: 5 * 60 * 1000 = no refetch for 5 minutes.

gcTime (default 5 minutes): how long unused data stays in the cache.

  • Data is "unused" when no mounted component subscribes to its key.
  • After gcTime, it's garbage-collected; the next useQuery starts with isPending again.
AspectstaleTimegcTime
ControlsWhen to refetchWhen to delete from cache
Clock startsWhen data was fetchedWhen the last subscriber unmounts
Default0 (always stale)5 minutes

What / Why / When

What

Automatic caching with configurable freshness.

Why

Navigating back to a page shows data instantly, and duplicate requests are avoided.

When

Raise staleTime for data that rarely changes; lower gcTime for large or sensitive data.

Code compare

React
useQuery({  queryKey: ["events", id],  queryFn: ({ signal }) => fetchEvent({ id, signal }),  staleTime: 5 * 60 * 1000, // fresh for 5 minutes  gcTime: 30 * 60 * 1000, // kept 30 minutes after last use});

Angular has no built-in shared query cache. A root service with signals provides the same behavior for the data that needs it:

Angular
// services/events-cache.service.ts@Injectable({ providedIn: "root" })export class EventsCacheService {  private eventsService = inject(EventsService);  private cache = new Map<string, { data: EventItem[]; fetchedAt: number }>();  private staleTime = 5 * 60 * 1000;​  getEvents(params: { search?: string; max?: number } = {}): Observable<EventItem[]> {    const key = JSON.stringify(params);    const cached = this.cache.get(key);​    if (cached && Date.now() - cached.fetchedAt < this.staleTime) {      return of(cached.data); // fresh: no request    }​    return this.eventsService      .getEvents(params)      .pipe(tap((data) => this.cache.set(key, { data, fetchedAt: Date.now() })));  }​  invalidate() {    this.cache.clear();  }}

Differences and gotchas

  • This Angular version covers staleTime only. Data lives as long as the service (no gcTime), and it doesn't show stale data while refetching. Add those only if the app needs them.
  • TanStack Query also refetches stale data on window focus. Disable with refetchOnWindowFocus: false if that's surprising during development.

Quick reference

FeatureReactAngular
Shared cacheQueryClientRoot service + Map/signals
FreshnessstaleTimeTimestamp check in service
CleanupgcTimeService lifetime (manual if needed)
Instant cached UIBuilt inof(cached)