React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

swrcachededuplicationcache-provider

Caching Strategies

Recipe

SWR uses an in-memory cache by default. Customize the cache provider for persistent storage, design your key strategy for maximum cache hits, and leverage deduplication to avoid redundant network requests.

"use client";
 
import { SWRConfig } from "swr";
 
function App({ children }: { children: React.ReactNode }) {
  return (
    <SWRConfig
      value={{
        provider: () => new Map(),  // Default in-memory cache
        dedupingInterval: 2000,     // Dedup requests within 2s
      }}
    >
      {children}
    </SWRConfig>
  );
}

Working Example

"use client";
 
import { SWRConfig, Cache } from "swr";
import useSWR from "swr";
 
// Persistent cache backed by localStorage
function localStorageProvider(): Cache {
  const map = new Map<string, any>(
    JSON.parse(localStorage.getItem("swr-cache") || "[]")
  );
 
  // Persist to localStorage before page unload
  window.addEventListener("beforeunload", () => {
    const appCache = JSON.stringify(Array.from(map.entries()));
    localStorage.setItem("swr-cache", appCache);
  });
 
  return {
    get: (key: string) => map.get(key),
    set: (key: string, value: any) => map.set(key, value),
    delete: (key: string) => map.delete(key),
    keys: () => map.keys(),
  };
}
 
export function CachedApp({ children }: { children: React.ReactNode }) {
  return (
    <SWRConfig value={{ provider: localStorageProvider }}>
      {children}
    </SWRConfig>
  );
}
 
// Components automatically benefit from persistent cache
function UserProfile() {
  const { data } = useSWR("/api/me", (url) => fetch(url).then((r) => r.json()));
  return <div>{data?.name}</div>;
}

Deep Dive

How It Works

  • SWR's cache is a simple key-value store. The key from useSWR(key) maps directly to a cache entry.
  • The default cache is a Map instance that lives in memory and resets on page reload.
  • Deduplication: When multiple components request the same key within dedupingInterval ms, only one fetch runs. All hooks receive the same promise.
  • Cache entries store data, error, and metadata (timestamps). SWR uses these to decide if data is stale.
  • The provider function in SWRConfig is called once on mount and must return a Cache-compatible object.

Variations

Key design for cache efficiency:

// Good: Stable, serializable keys
useSWR(`/api/users/${id}`, fetcher);
useSWR(["/api/users", id, "posts"], fetcher);
 
// Bad: Unstable object references (not serializable)
useSWR({ url: "/api/users", id }, fetcher); // Will not cache properly

Prefetching into cache:

import { preload } from "swr";
 
const fetcher = (url: string) => fetch(url).then((r) => r.json());
 
// Prefetch on hover or route prefetch
function ProductCard({ id }: { id: string }) {
  const handleMouseEnter = () => {
    preload(`/api/products/${id}`, fetcher);
  };
 
  return (
    <a href={`/products/${id}`} onMouseEnter={handleMouseEnter}>
      View Product
    </a>
  );
}

Clearing the cache:

import { useSWRConfig } from "swr";
 
function LogoutButton() {
  const { cache } = useSWRConfig();
 
  const handleLogout = () => {
    // Clear all cached data on logout
    if (cache instanceof Map) {
      cache.clear();
    }
  };
 
  return <button onClick={handleLogout}>Logout</button>;
}

Cache with TTL:

function ttlProvider(): Cache {
  const cache = new Map<string, { value: any; expiry: number }>();
  const TTL = 5 * 60 * 1000; // 5 minutes
 
  return {
    get: (key: string) => {
      const entry = cache.get(key);
      if (!entry) return undefined;
      if (Date.now() > entry.expiry) {
        cache.delete(key);
        return undefined;
      }
      return entry.value;
    },
    set: (key: string, value: any) => {
      cache.set(key, { value, expiry: Date.now() + TTL });
    },
    delete: (key: string) => cache.delete(key),
    keys: () => cache.keys(),
  };
}

TypeScript Notes

  • Import Cache from swr for typing custom providers.
  • The provider option expects () => Cache.
import type { Cache } from "swr";
 
const provider = (): Cache => {
  const map = new Map();
  return {
    get: (key: string) => map.get(key),
    set: (key: string, value: any) => map.set(key, value),
    delete: (key: string) => map.delete(key),
    keys: () => map.keys(),
  };
};

Gotchas

  • The default in-memory cache is lost on page reload. If you need persistence, implement a custom provider.
  • dedupingInterval only deduplicates concurrent requests. It does not prevent re-fetching after the interval expires.
  • Array keys like ["/api/users", id] are serialized for caching. The order of elements matters: [a, b] and [b, a] are different cache keys.
  • Custom cache providers must implement the full Cache interface including keys(). Missing methods will cause runtime errors.
  • localStorage has a ~5MB limit. Large caches can silently fail. Implement error handling in your provider's set method.
  • Do not share cache state between SWRConfig providers at different nesting levels; each provider creates its own scope.

Alternatives

ApproachProsCons
Default Map cacheZero config, fastLost on reload
localStorage providerPersists across sessions5MB limit, sync API blocks main thread
IndexedDB providerLarge storage, asyncMore complex implementation
No cache (dedupingInterval: 0)Always fresh dataMore network requests

FAQs

What is the default cache implementation in SWR?

SWR uses an in-memory Map instance by default. It resets on page reload. No configuration is needed for basic caching.

How do I make the SWR cache persist across page reloads?

Implement a custom cache provider backed by localStorage or IndexedDB. The provider must implement the Cache interface (get, set, delete, keys). Persist on beforeunload and rehydrate on initialization.

What is the difference between deduplication and caching in SWR?
  • Deduplication (dedupingInterval) prevents concurrent duplicate requests within a time window.
  • Caching stores data across the full session so subsequent renders use cached values.
  • Deduplication does not prevent re-fetching after the interval expires.
Gotcha: Does the order of elements in array keys matter for caching?

Yes. ["/api/users", id] and [id, "/api/users"] are different cache keys. SWR serializes array elements in order, so changing the order creates a different cache entry.

How do I prefetch data into the SWR cache?
import { preload } from "swr";
 
const fetcher = (url: string) => fetch(url).then((r) => r.json());
 
// Prefetch on hover
const handleMouseEnter = () => {
  preload(`/api/products/${id}`, fetcher);
};
How do I clear the entire SWR cache (e.g., on logout)?
const { cache } = useSWRConfig();
 
const handleLogout = () => {
  if (cache instanceof Map) {
    cache.clear();
  }
};
Can I use object keys for SWR caching?

No. Object references are not serializable by SWR. Use strings or arrays as keys. useSWR({ url: "/api/users", id }, fetcher) will not cache properly.

Gotcha: What happens if localStorage exceeds its ~5MB limit in a custom provider?

The write silently fails. Implement error handling in your provider's set method using a try/catch, and consider evicting old entries or falling back to in-memory storage.

How do I type a custom cache provider in TypeScript?
import type { Cache } from "swr";
 
const provider = (): Cache => {
  const map = new Map();
  return {
    get: (key: string) => map.get(key),
    set: (key: string, value: any) => map.set(key, value),
    delete: (key: string) => map.delete(key),
    keys: () => map.keys(),
  };
};
What TypeScript type should the provider option be in SWRConfig?

The provider option expects a function of type () => Cache. Import Cache from swr to type your custom implementation. Missing interface methods (keys, delete) will cause runtime errors.

How does a TTL-based cache provider work with SWR?

Store entries with an expiry timestamp. On get, check if the entry has expired and return undefined if so. This forces SWR to treat it as a cache miss and re-fetch.