We use cookies to enhance your experience on the site
CodeWorlds

Data Caching and Stale-While-Revalidate

Imagine that your spaceship repeatedly asks Mission Control for the same planetary data. Every time you wait for a response, even if the data hasn't changed. That's wasteful! The solution is caching -- storing data locally and refreshing it in the background.

Why is Caching Important?

Without cache:

  • Every navigation = new request = spinner
  • User sees "Loading..." even for data they've already seen
  • Unnecessary server and network load

With cache:

  • Data displayed instantly from memory
  • Background refresh (no spinner)
  • Better user experience

The Stale-While-Revalidate (SWR) Pattern

SWR is a caching strategy used by browsers and libraries. The name says it all:

  1. Stale -- immediately show data from cache (even if it might be outdated)
  2. While-Revalidate -- simultaneously fetch fresh data from the server in the background
  3. After receiving fresh data -- update the view
1// SWR visualization:
2// Step 1: User enters the page
3//   -> Cache exists? YES -> Show cached data (instantly!)
4//   -> Simultaneously: send request to server
5
6// Step 2: Server responds
7//   -> Data changed? YES -> Update view and cache
8//   -> Same data? -> Do nothing
9
10// Step 3: User returns to the page
11//   -> Repeat from Step 1 (instant display!)

Simple Cache with Map

The simplest cache can be built with a

Map
object:

1// Global cache - lives outside components
2const cache = new Map();
3
4function useFetchWithCache(url, ttl = 60000) {
5  const [data, setData] = useState(() => {
6    const cached = cache.get(url);
7    if (cached && Date.now() - cached.timestamp < ttl) {
8      return cached.data; // Instant data from cache!
9    }
10    return null;
11  });
12  const [loading, setLoading] = useState(!cache.has(url));
13  const [error, setError] = useState(null);
14
15  useEffect(() => {
16    const controller = new AbortController();
17
18    async function fetchData() {
19      try {
20        // If we have cache - don't show spinner
21        if (!cache.has(url)) setLoading(true);
22        setError(null);
23
24        const response = await fetch(url, {
25          signal: controller.signal,
26        });
27        if (!response.ok) {
28          throw new Error(\`HTTP \${response.status}\`);
29        }
30        const result = await response.json();
31
32        // Save to cache with timestamp
33        cache.set(url, {
34          data: result,
35          timestamp: Date.now(),
36        });
37        setData(result);
38      } catch (err) {
39        if (err.name !== 'AbortError') {
40          setError(err.message);
41        }
42      } finally {
43        setLoading(false);
44      }
45    }
46
47    fetchData();
48    return () => controller.abort();
49  }, [url, ttl]);
50
51  return { data, loading, error };
52}

TTL (Time To Live) - Cache Lifetime

TTL determines how long cached data is considered fresh:

1// TTL = 60000ms (1 minute)
2// Data older than 1 minute will be fetched again
3
4// Short TTL (5-30s) - frequently changing data
5// e.g., cryptocurrency prices, mission status
6const liveData = useFetchWithCache('/api/status', 5000);
7
8// Medium TTL (1-5 min) - moderately changing data
9// e.g., planet list, ship catalog
10const catalog = useFetchWithCache('/api/planets', 60000);
11
12// Long TTL (10-60 min) - rarely changing data
13// e.g., configuration, static lists
14const config = useFetchWithCache('/api/config', 600000);

Cache Invalidation - Forcing a Refresh

Sometimes you need to force fresh data retrieval (e.g., after adding a new item):

1// Simple invalidation - remove from cache
2function invalidateCache(url) {
3  cache.delete(url);
4}
5
6// After adding a new mission - invalidate the missions list
7async function createMission(data) {
8  await fetch('/api/missions', {
9    method: 'POST',
10    headers: { 'Content-Type': 'application/json' },
11    body: JSON.stringify(data),
12  });
13
14  // Force refresh of missions list
15  invalidateCache('/api/missions');
16}

Optimistic Updates - Updating Before the Response

Instead of waiting for the server, we can update the UI immediately:

1function MissionList() {
2  const [missions, setMissions] = useState([]);
3
4  const deleteMission = async (id) => {
5    // 1. OPTIMISTICALLY remove from UI (instantly!)
6    const previousMissions = [...missions];
7    setMissions(prev => prev.filter(m => m.id !== id));
8
9    try {
10      // 2. Send DELETE request to server
11      await fetch(\`/api/missions/\${id}\`, {
12        method: 'DELETE',
13      });
14      // Success - UI already updated!
15    } catch (error) {
16      // 3. ERROR - restore previous state (rollback)
17      setMissions(previousMissions);
18      alert('Failed to delete mission');
19    }
20  };
21
22  return (
23    <ul>
24      {missions.map(m => (
25        <li key={m.id}>
26          {m.name}
27          <button onClick={() => deleteMission(m.id)}>
28            Delete
29          </button>
30        </li>
31      ))}
32    </ul>
33  );
34}

TanStack Query -- Professional Caching

In large projects, instead of writing your own cache, we use the TanStack Query library (formerly React Query). It's like an advanced onboard computer that automatically manages all communication:

1import {
2  useQuery,
3  useMutation,
4  useQueryClient,
5  QueryClient,
6  QueryClientProvider,
7} from '@tanstack/react-query';
8
9const queryClient = new QueryClient();
10
11function App() {
12  return (
13    <QueryClientProvider client={queryClient}>
14      <PlanetList />
15    </QueryClientProvider>
16  );
17}
18
19function PlanetList() {
20  // useQuery - automatic cache, SWR, retry, refetch
21  const { data, isLoading, error } = useQuery({
22    queryKey: ['planets'],
23    queryFn: () =>
24      fetch('https://swapi.dev/api/planets/')
25        .then(res => res.json()),
26    staleTime: 60000, // Data fresh for 1 minute
27    gcTime: 300000, // Remove from cache after 5 minutes
28  });
29
30  if (isLoading) return <p>Scanning the galaxy...</p>;
31  if (error) return <p>Error: {error.message}</p>;
32
33  return (
34    <ul>
35      {data.results.map(planet => (
36        <li key={planet.name}>{planet.name}</li>
37      ))}
38    </ul>
39  );
40}

TanStack Query - Key Concepts

1// 1. queryKey - unique data identifier in cache
2//    Changing the key = new request
3const planets = useQuery({
4  queryKey: ['planets', page],
5  queryFn: () => fetchPlanets(page),
6});
7
8// 2. staleTime - how long data stays fresh
9//    staleTime: 0 - always refresh (default)
10//    staleTime: 60000 - fresh for 1 minute
11//    staleTime: Infinity - never refresh
12
13// 3. gcTime (garbage collection) - when to remove from cache
14//    gcTime: 300000 - remove after 5 min of inactivity
15
16// 4. Automatic refetch:
17//    - When the window regains focus
18//    - When network reconnects after disconnection
19//    - At regular intervals (refetchInterval)
20
21// 5. useMutation - write operations with invalidation
22const mutation = useMutation({
23  mutationFn: (newMission) =>
24    fetch('/api/missions', {
25      method: 'POST',
26      body: JSON.stringify(newMission),
27    }),
28  onSuccess: () => {
29    // After adding a mission - invalidate the missions list cache
30    queryClient.invalidateQueries({
31      queryKey: ['missions']
32    });
33  },
34});

Caching is the superpower of every cosmic navigator -- data is available instantly, the server isn't overloaded, and the user never waits for data they've already seen!

Go to CodeWorlds