Caching Strategies Compared: SWR, TanStack Query and Apollo Client
AI generated
</>
{ }
React · SWR · TanStack Query · Apollo Client
Caching Strategies Compared
SWR, TanStack Query and Apollo Client

Caching strategies decide how often a React application reloads data, how quickly changes become visible, and how much memory the client spends on it. SWR, TanStack Query and Apollo Client solve the same basic task with different caching strategies, from simple stale while revalidate to full object normalization, with noticeable consequences for consistency and maintenance effort.

18 min read Stale While Revalidate · Query Keys · Normalization · Garbage Collection React 19 · SWR 2 · TanStack Query v5 · Apollo Client 3.x

1. Why caching strategies are the biggest performance lever

Before looking at a concrete library, it is worth looking at the shared underlying problem each of the three solutions tries to solve.

The caching strategies of a React application decide perceived speed more than almost any other architectural decision. A component that reloads from the server on every mount feels slow, even if the server responds in under a hundred milliseconds, because users perceive a visible loading state. A well thought out caching strategy instead shows the most recently known data immediately and updates in the background, which makes the application feel practically instant.

SWR, TanStack Query and Apollo Client pursue different caching strategies that look similar at first glance but differ considerably in the details: from how individual cache entries are addressed, through invalidation logic, to whether object normalization happens at all. This article compares the three approaches along exactly these criteria and shows when which caching strategy delivers the greatest practical advantage.

Another reason the detailed comparison is worthwhile: all three libraries use the same marketing term, caching, but hide different technical realities behind it. Anyone choosing a library purely by popularity or GitHub stars often overlooks that the underlying caching strategy gives a completely different answer to the same question, namely how long data stays valid and when it gets refreshed.

2. SWR: stale while revalidate in its purest form

SWR, developed by Vercel, is named after exactly the HTTP cache header concept it implements: stale while revalidate. The caching strategy immediately shows the potentially stale data present in the cache while a new request runs in the background, and updates the UI once the response arrives. This caching strategy is deliberately kept minimalist: a cache key, usually the URL, addresses an entry, without more complex query objects or normalization.

SWR is often used as the first caching library in smaller projects or prototypes, precisely because the entry barrier is minimal and hardly any concepts are assumed beforehand.

This simplicity is SWR's greatest strength and, at the same time, its greatest limitation. For applications with mostly independent data sources, where an object is rarely loaded through several different endpoints, SWR's lean caching strategy is sufficient and produces minimal overhead. For applications with heavily interlinked data models, where the same object must stay consistent across several views, this caching strategy hits its limits without additional manual work.


// useUser.ts — SWR's minimal caching strategy, keyed by URL
import useSWR from "swr";

const fetcher = (url: string) => fetch(url).then((res) => res.json());

function useUser(userId: string) {
  return useSWR(`/api/users/${userId}`, fetcher, {
    revalidateOnFocus: true,
    dedupingInterval: 5000, // Ignore duplicate requests within 5s
  });
}

// Manual revalidation after a mutation, since SWR has no automatic normalization
import { mutate } from "swr";

async function updateUserName(userId: string, name: string) {
  await fetch(`/api/users/${userId}`, { method: "PATCH", body: JSON.stringify({ name }) });
  // Revalidate this specific cache key
  mutate(`/api/users/${userId}`);
}

SWR is also closely tied to Next.js, but was deliberately kept framework agnostic and works just as well in pure client side React applications. For teams already invested in the Vercel ecosystem, integration is often seamless, because documentation and examples are frequently tailored directly to Next.js patterns, which further accelerates getting started with this caching strategy.

3. TanStack Query: query keys as cache addressing

TanStack Query extends SWR's stale while revalidate principle with a considerably more expressive addressing system: query keys are arrays instead of single strings, which allows modeling hierarchical relationships between cache entries, for example ["users", userId, "posts"]. This caching strategy allows invalidateQueries to target not only an exact key, but entire subtrees of query keys, without having to enumerate every affected key individually.

Another difference from SWR is built in support for mutations as a first class concept with onMutate, onError and onSettled, instead of leaving mutations entirely to the developer. This caching strategy brings structured tools for exactly the cases that would have to be rebuilt manually with SWR, such as optimistic updates with rollback. For medium to large applications with many mutations, this structured approach is usually the more practical one.


// useUserPosts.ts — TanStack Query's hierarchical query-key strategy
import { useQuery, useQueryClient } from "@tanstack/react-query";

function useUserPosts(userId: string) {
  return useQuery({
    queryKey: ["users", userId, "posts"],
    queryFn: () => fetchUserPosts(userId),
    staleTime: 60_000, // Data considered fresh for 60s, no background refetch
  });
}

function useInvalidateUser(userId: string) {
  const queryClient = useQueryClient();
  return () => {
    // Invalidates the whole subtree: user data, posts, comments, everything nested
    queryClient.invalidateQueries({ queryKey: ["users", userId] });
  };
}

An often overlooked advantage of TanStack Query is built in support for parallel and dependent queries through useQueries and the enabled option, which lets a query start only once a previous query has completed successfully. These building blocks of TanStack Query's caching strategy cover use cases that with SWR frequently require additional, self written coordination logic.

4. Apollo Client: normalized object graph instead of a query cache

Apollo Client pursues a fundamentally different caching strategy than SWR and TanStack Query: instead of caching results per request or query key, InMemoryCache breaks down every response into individual, normalized objects, referenced through __typename and id. This caching strategy ensures that a single object, regardless of which query originally loaded it, stays consistent everywhere in the cache once it changes.

The price for this consistency is additional complexity: type policies, keyFields and field level merge functions have to be configured correctly for normalization to actually work. For applications where the same product or the same user appears in dozens of views, this more elaborate caching strategy pays off, because a single update propagates automatically everywhere, without developers having to manually think through which query keys might be affected.

This object based caching strategy shows its strength especially in forms that make the same field of an object editable simultaneously in several views, for example an inline edit in a table and a full edit form in a modal. Both views automatically read the same normalized cache entry, so a change in one place becomes visible instantly and without extra code in the other place.

5. Invalidation compared: tags, keys and object IDs

The three libraries invalidate cache entries in fundamentally different ways. SWR invalidates through the exact cache key, usually the URL, which means developers have to know precisely which URLs are affected by a mutation. TanStack Query invalidates through query key prefixes, which captures entire subtrees at once and requires considerably less manual bookkeeping, as long as the key structure was thought through from the start.

Apollo Client invalidates implicitly, by updating the affected object directly in the normalized cache instead of invalidating a query: every component referencing that object through any query sees the change automatically. This caching strategy needs no explicit invalidation system for individual object changes, but requires careful configuration of normalization itself so this automatism actually kicks in.


// Comparing invalidation across the three caching strategies
// SWR: invalidate by exact key
mutate(`/api/users/${userId}`);

// TanStack Query: invalidate a whole subtree by key prefix
queryClient.invalidateQueries({ queryKey: ["users", userId] });

// Apollo Client: no explicit invalidation call needed for a single field change,
// cache.modify updates the object directly, all referencing views update automatically
cache.modify({
  id: cache.identify({ __typename: "User", id: userId }),
  fields: { name: () => newName },
});

A practical middle ground for teams using TanStack Query but occasionally wanting to benefit from object based consistency is a thin, self written normalization layer that keeps known entities such as users or products in a central map keyed by ID and synchronously updates them on every query result. This is no replacement for Apollo's fully automatic caching strategy, but it noticeably reduces inconsistencies for critical, frequently reused entities, without switching the entire caching model.

6. Staleness configuration: staleTime, dedupingInterval and fetchPolicy

Each of the three libraries offers its own mechanism to define how long data is considered fresh before a background refetch is triggered. SWR uses dedupingInterval to collapse multiple identical requests within a short time window into a single one, but revalidates by default on every window focus and every network reconnection. TanStack Query uses staleTime as an explicit period during which data counts as fresh and no automatic refetch happens, allowing finer grained control per query.

Apollo Client configures this behavior via fetchPolicy, such as cache-first, cache-and-network or network-only, with this setting controlled more fundamentally per query rather than through a duration. These different models for the same basic caching strategy reflect different philosophies: SWR optimizes for simplicity with sensible defaults, TanStack Query for explicit control, and Apollo Client for object graph consistency over time.

A practical tip for all three libraries: different data types should get different staleness values. User profiles rarely change and tolerate a high staleTime of several minutes, while price information in a shopping cart needs to be nearly real time. A uniform, global staleness configuration for the entire caching strategy overlooks these differences and leads either to unnecessary refetches or to noticeably stale data at critical points.

7. Memory behavior and garbage collection

Without garbage collection, every caching strategy would grow indefinitely as soon as an application is used for hours and new data keeps entering the cache. TanStack Query solves this via gcTime, formerly called cacheTime: unused query results that no active component accesses anymore are automatically removed from memory once this period elapses. SWR follows a similar approach, but is more generous by default and often requires explicit configuration for aggressive memory cleanup.

Apollo Client offers cache.gc() as a manually triggerable garbage collection that removes every object no active query references anymore, but typically needs to be called explicitly after cache.evict() calls. For applications with very large datasets and long sessions, such as admin dashboards that stay open for hours, this memory behavior is an often underestimated factor when choosing a caching strategy, because an uncontrolled growing cache noticeably slows down the application over time, even with correctly displayed data.

An often overlooked side effect of missing garbage collection shows up in single page applications with client side routing: as a user navigates between many different detail pages, without cleanup, the cache entries of every ever visited page accumulate, even long after the user has moved to a completely different part of the application. A sensibly configured caching strategy with active garbage collection instead keeps memory usage proportional to actual active usage, not to the entire interaction history of a session.

8. Persistence and offline behavior of the three caches

All three libraries support cache persistence through storage adapters, which periodically back up the in memory cache to localStorage or IndexedDB so a page reload can start immediately with known data. TanStack Query offers persistQueryClient as an official plugin for this, SWR uses a configurable provider, and Apollo Client offers apollo3-cache-persist as a community maintained extension.

For true offline functionality, where mutations are queued during a network interruption and automatically resent later, TanStack Query offers the most mature built in mechanism among the three with its mutation queue functionality. Apollo Client, with its Apollo Link architecture, does provide the building blocks for a custom offline queue, but demands considerably more implementation work of your own than TanStack Query's built in solution.

For progressive web apps with explicit offline requirements, it is worth planning cache persistence early in the project regardless of the chosen caching strategy, instead of bolting it on afterward. The choice of storage adapter, IndexedDB for larger datasets, localStorage for smaller ones, directly influences how much data stays available offline and how quickly the application becomes usable again after a cold start.

9. Caching strategies compared directly

The following table summarizes the decisive differences between the three caching strategies and serves as a decision basis for the next project architecture. It does not replace an individual analysis of your own data model, but makes the central tradeoffs visible at a glance.

Criterion SWR TanStack Query Apollo Client
Cache addressing URL string Hierarchical query keys Object based (Typename:id)
Normalization None None Full
Mutation tools Minimal, manual onMutate/onError/onSettled optimisticResponse, cache.modify
Bundle size Very small Small to medium Large
Learning curve Low Medium High
Ideal use case Simple REST applications REST and mixed data sources Complex, GraphQL based data models

An aspect not directly visible in the table is migration friendliness: switching from SWR to TanStack Query is usually straightforward, because both rely on similar concepts such as hooks and cache keys. Switching from or to Apollo Client is considerably more work, because it usually also means switching the underlying data layer from REST to GraphQL or vice versa, which goes far beyond just swapping the caching strategy.

None of these caching strategies is superior in every context. SWR suits teams wanting minimal overhead and a fast start. TanStack Query offers the best balance of control and simplicity for most REST and mixed applications. Apollo Client pays off once a GraphQL data model with heavily interlinked objects must stay consistent across many views.

This decision should, like any fundamental architecture question, be documented and shared with the team, so later extensions to the application consistently continue the same caching strategy, instead of establishing several competing approaches in parallel within the same project.

Mironsoft

Caching architecture for React with SWR, TanStack Query and Apollo Client

The right caching strategy for your data volume?

We analyze your data model and recommend the caching library that actually fits your consistency requirements, team size and bundle budget.

Cache audit

Analysis of existing invalidation logic for redundant and missing refetches

Migration

Switching between SWR, TanStack Query and Apollo Client without big bang risk

Memory tuning

Configuring gcTime, cache.gc() and persistence for long running sessions

10. Summary

The choice between SWR, TanStack Query and Apollo Client ultimately remains a question of actual consistency requirements, not the raw popularity of a library.

If unsure, start with the smallest sensible caching strategy for the current use case and only switch to a more elaborate solution once concrete consistency or performance needs arise, instead of choosing the most complex option from the start.

The three compared caching strategies solve the same basic task with different effort and different precision. SWR's stale while revalidate over simple URL keys suits lean applications with mostly independent data sources. TanStack Query's hierarchical query keys offer more control over invalidation and structured mutation tools, without the complexity of full object normalization.

Regardless of the chosen library: different data types deserve different staleness values, and garbage collection should never be left to chance, especially in long running sessions such as admin dashboards.

Apollo Client's normalized object graph remains the most elaborate, but also the most consistent, caching strategy for heavily interlinked GraphQL data models. Staleness configuration via staleTime, dedupingInterval or fetchPolicy, garbage collection via gcTime or cache.gc(), and persistence adapters for offline scenarios round out each respective caching model. The right choice depends less on personal preference than on the actual structure of the data and the size of the application.

Switching caching strategy mid project remains possible, but should always happen gradually and with clear success criteria per migration section, instead of swapping the entire data layer in a single, risky step.

Caching strategies compared, the essentials at a glance

SWR

Minimalist stale while revalidate over URL keys, ideal for lean applications.

TanStack Query

Hierarchical query keys, structured mutations, good balance of control and simplicity.

Apollo Client

Full object normalization, ideal for complex GraphQL data models.

Decision

Data model complexity and consistency requirements decide, not library popularity.

All three caching strategies benefit from data type specific staleness configuration instead of one global value for the entire application.

The following frequently asked questions summarize the key decision points around caching strategies once more, compactly.

11. FAQ: Caching strategies compared

1What does stale while revalidate mean?
The cache immediately shows known data while a new request runs in the background and the UI updates afterward.
2Why no normalization in SWR?
SWR is deliberately minimalist and caches per URL key, instead of recognizing and normalizing objects in the payload.
3Advantage of hierarchical query keys?
Entire subtrees can be invalidated with one invalidateQueries call, instead of enumerating every key individually.
4When does Apollo's normalized cache pay off?
When the same object must stay consistent across many views and updates should propagate automatically.
5What happens without garbage collection?
The cache grows indefinitely and memory usage rises noticeably over hours.
6staleTime vs. gcTime?
staleTime determines freshness without refetch, gcTime determines how long unused data stays in memory.
7Does SWR offer optimistic updates?
Yes, via mutate with optimisticData, but less structured than TanStack Query's onMutate/onError.
8Best strategy for offline functionality?
TanStack Query's mutation queue functionality is the most mature built in mechanism for this.
9Apollo Client for REST APIs?
Technically possible, but normalization benefits only unfold with an actual GraphQL schema.
10Switching between strategies worthwhile?
Only gradually, feature by feature, with clear validation after each migration section.