Skip to content
intermediatePhase 37 · Frontend Architecture

Client-Side Caching

Cache API responses with stale-while-revalidate and cache invalidation.

45m
0 problems
Topic Progress0%

Cache Strategies

Cache Strategies

Different approaches to caching based on use case.

Cache-Aside (Lazy Loading)

// Most common pattern - check cache first, fetch if missing
function useProduct(id) {
  return useQuery({
    queryKey: ['product', id],
    queryFn: () => fetchProduct(id),
    staleTime: 5 * 60 * 1000, // 5 minutes
    gcTime: 30 * 60 * 1000, // 30 minutes (garbage collection)
  });
}

// Usage
function ProductPage({ id }) {
  const { data, isLoading, isStale } = useProduct(id);
  
  return (
    <div>
      {isLoading ? <Spinner /> : (
        <>
          <h1>{data.name}</h1>
          {isStale && <span className="stale-indicator">Refreshing...</span>}
        </>
      )}
    </div>
  );
}

Read-Through Cache

// Cache always returns data, fetching in background
function useProductsWithReadThrough() {
  return useQuery({
    queryKey: ['products'],
    queryFn: fetchProducts,
    staleTime: 5 * 60 * 1000,
    refetchOnMount: 'always', // Always refetch on mount
    refetchOnWindowFocus: true, // Refetch when window gains focus
  });
}

Write-Through Cache

// Update cache immediately on write
const addProduct = useMutation({
  mutationFn: createProduct,
  onSuccess: (newProduct) => {
    queryClient.setQueryData(['products'], (old) => [...old, newProduct]);
  },
});

Write-Behind (Write-Back)

// Queue writes and batch them
function useWriteBehindCache() {
  const pendingWrites = useRef([]);
  
  const queueWrite = useCallback((operation) => {
    pendingWrites.current.push(operation);
    
    // Debounce batch write
    if (pendingWrites.current.length === 1) {
      setTimeout(() => flushWrites(), 1000);
    }
  }, []);
  
  const flushWrites = async () => {
    const writes = pendingWrites.current.splice(0);
    if (writes.length === 0) return;
    
    try {
      await batchAPI(writes);
    } catch (error) {
      // Re-queue failed writes
      pendingWrites.current.unshift(...writes);
    }
  };
  
  return { queueWrite };
}

Choosing a Strategy

Strategy Use Case
Cache-Aside General purpose, read-heavy
Read-Through Frequently accessed data
Write-Through Data consistency critical
Write-Behind High write throughput

Stale-While-Revalidate

Stale-While-Revalidate

Show cached data immediately while fetching fresh data in the background.

React Query Implementation

// Default stale-while-revalidate behavior
function useProducts() {
  return useQuery({
    queryKey: ['products'],
    queryFn: fetchProducts,
    staleTime: 60 * 1000, // Data is fresh for 1 minute
    gcTime: 5 * 60 * 1000, // Keep in cache for 5 minutes
  });
}

// Configure per query
function useProduct(id) {
  return useQuery({
    queryKey: ['product', id],
    queryFn: () => fetchProduct(id),
    staleTime: id === 'featured' ? 0 : 60000, // Featured products always fresh
    gcTime: Infinity, // Never remove from cache
  });
}

Visual Indicators

function ProductList() {
  const { data, isLoading, isFetching, isStale } = useProducts();

  return (
    <div>
      {isLoading ? (
        <Spinner />
      ) : (
        <>
          {isStale && isFetching && (
            <div className="refresh-banner">
              Updating data...
            </div>
          )}
          <ProductGrid products={data} />
        </>
      )}
    </div>
  );
}

Custom Stale-While-Revalidate Hook

function useSWRHook(key, fetcher, options = {}) {
  const { dedupingInterval = 2000 } = options;
  const [data, setData] = useState(() => cache.get(key));
  const [isValidating, setIsValidating] = useState(false);

  useEffect(() => {
    let cancelled = false;
    const lastUpdated = cache.getTimestamp(key);
    const isStale = !lastUpdated || 
      Date.now() - lastUpdated > (options.staleTime || 0);

    if (isStale) {
      setIsValidating(true);
      fetcher(key)
        .then((result) => {
          if (!cancelled) {
            setData(result);
            cache.set(key, result);
          }
        })
        .finally(() => {
          if (!cancelled) setIsValidating(false);
        });
    }

    return () => { cancelled = true; };
  }, [key, fetcher, options.staleTime]);

  return { data, isValidating, isStale: !cache.getTimestamp(key) };
}

Benefits

  • Instant UI: Show cached data immediately
  • Background updates: Fetch fresh data without blocking
  • Bandwidth efficient: Only fetch when stale
  • Better UX: No loading spinners for cached data

Cache Invalidation

Cache Invalidation

Keep cache consistent with server state.

Invalidation Strategies

// Exact key invalidation
queryClient.invalidateQueries({ queryKey: ['product', 123] });

// Partial key match
queryClient.invalidateQueries({ queryKey: ['products'] });

// All queries
queryClient.invalidateQueries();

// Predicate-based
queryClient.invalidateQueries({
  predicate: (query) => query.queryKey[0] === 'products',
});

Mutation-Based Invalidation

// After any mutation, invalidate related queries
const updateProduct = useMutation({
  mutationFn: (product) =>
    fetch(`/api/products/${product.id}`, {
      method: 'PUT',
      body: JSON.stringify(product),
    }),
  onSuccess: (data, product) => {
    // Invalidate specific product
    queryClient.invalidateQueries({ queryKey: ['product', product.id] });
    
    // Invalidate product list
    queryClient.invalidateQueries({ queryKey: ['products'] });
    
    // Invalidate any query starting with 'products'
    queryClient.invalidateQueries({ queryKey: ['products'] });
  },
});

Optimistic Invalidation

const deleteProduct = useMutation({
  mutationFn: (id) => fetch(`/api/products/${id}`, { method: 'DELETE' }),
  onMutate: async (id) => {
    await queryClient.cancelQueries({ queryKey: ['products'] });
    const previous = queryClient.getQueryData(['products']);
    
    // Optimistically remove
    queryClient.setQueryData(['products'], (old) =>
      old.filter((p) => p.id !== id)
    );
    
    return { previous };
  },
  onError: (err, id, context) => {
    queryClient.setQueryData(['products'], context.previous);
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['products'] });
  },
});

Cache Time Configuration

// Global defaults
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      gcTime: 30 * 60 * 1000, // 30 minutes
      refetchOnWindowFocus: true,
      refetchOnMount: true,
      refetchOnReconnect: true,
    },
  },
});

// Per-query overrides
useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  staleTime: Infinity, // Never go stale (user data rarely changes)
  gcTime: Infinity, // Keep in cache forever
});

Common Pitfalls

  1. Over-invalidation: Invalidating too many queries causes unnecessary refetches
  2. Under-invalidation: Missing invalidation causes stale data
  3. Forgetting to invalidate: Mutations without invalidation leave cache stale
  4. Not handling errors: Failed mutations should not invalidate cache

Practice Problems

0/3solved
Build Client-Side Caching Component

Create a reusable React component implementing Client-Side Caching. Include proper state management and accessibility.

Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where needed
Client-Side Caching Testing

Write unit and integration tests for Client-Side Caching using React Testing Library.

Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility tests
Client-Side Caching Performance

Optimize Client-Side Caching for performance. Consider memoization, code splitting, and bundle size.

Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysis

Quiz

1. What is the cache-aside pattern?

Question 1 options

2. What does staleTime control?

Question 2 options

3. When should you invalidate cache?

Question 3 options

4. What is the benefit of stale-while-revalidate?

Question 4 options

5. What is garbage collection in caching?

Question 5 options

Flashcards

Question

What is cache-aside pattern?

Answer

Check cache first; if missing or stale, fetch from server and update cache.

Question

What is stale-while-revalidate?

Answer

Show cached (possibly stale) data immediately, then fetch fresh data in the background.

Question

When should you invalidate cache?

Answer

After mutations that change server state, to keep the UI consistent with the server.

Question

What is the difference between staleTime and gcTime?

Answer

staleTime is when data needs refetching; gcTime is when data is removed from cache entirely.

Question

What is Client-Side Caching?

Answer

Client-Side Caching is a key concept in frontend development.

Revision Notes

Key Takeaways

  • 1.Choose cache strategy based on read/write patterns
  • 2.stale-while-revalidate provides instant UI with background updates
  • 3.Always invalidate cache after mutations that change server state
  • 4.Configure staleTime and gcTime based on data freshness needs
  • 5.Use visual indicators for data freshness status

Interview Tips

  • Explain the cache-aside pattern and when to use it
  • Describe stale-while-revalidate and its benefits
  • Discuss cache invalidation strategies and pitfalls

Cheat Sheet

Client-Side Caching Cheat Sheet

React Query Cache Config

useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
  staleTime: 60000, // 1 min fresh
  gcTime: 300000, // 5 min in cache
});

Invalidation

// Invalidate specific
queryClient.invalidateQueries({ queryKey: ['product', id] });
// Invalidate all with prefix
queryClient.invalidateQueries({ queryKey: ['products'] });

Cache Strategies

  • Cache-Aside: Check cache, fetch if needed
  • Read-Through: Cache fetches automatically
  • Write-Through: Write to cache and server
  • Write-Behind: Queue writes for batch