SWR

React hooks for data fetching with stale-while-revalidate strategy by Vercel

SWR - Stale-While-Revalidate

SWR is a React hooks library for data fetching created by Vercel. The name comes from the HTTP cache invalidation strategy "stale-while-revalidate" - it returns cached (stale) data first, then fetches fresh data in the background. It's lightweight, fast, and integrates seamlessly with Next.js.

Key Features

  • Lightweight — Only ~4KB gzipped
  • Fast — Instant UI with cached data
  • Revalidation — Automatic background updates
  • Focus Revalidation — Refetch when tab regains focus
  • Next.js Integration — Works great with Next.js

Installation

npm install swr

Basic Usage

import useSWR from 'swr';

// Fetcher function
const fetcher = (url) => fetch(url).then(res => res.json());

function Profile() {
  const { data, error, isLoading, mutate } = useSWR('/api/user', fetcher);
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Failed to load</div>;
  
  return (
    <div>
      <h1>Hello, {data.name}!</h1>
      <button onClick={() => mutate()}>Refresh</button>
    </div>
  );
}

Global Configuration

import { SWRConfig } from 'swr';

// Global fetcher and options
function App() {
  return (
    <SWRConfig
      value={{
        fetcher: (url) => fetch(url).then(res => res.json()),
        refreshInterval: 3000,        // Refresh every 3s
        revalidateOnFocus: true,      // Refetch on window focus
        dedupingInterval: 2000,       // Dedupe requests within 2s
        onError: (error) => {
          console.error('SWR Error:', error);
        },
      }}
    >
      <MyApp />
    </SWRConfig>
  );
}

// Now components don't need to specify fetcher
function UserProfile() {
  const { data } = useSWR('/api/user');
  return <div>{data?.name}</div>;
}

Dynamic Keys

import useSWR from 'swr';

function UserPosts({ userId }) {
  // Key can be string, array, or null
  const { data: posts } = useSWR(
    userId ? `/api/users/${userId}/posts` : null,
    fetcher
  );
  
  // Array keys for complex fetchers
  const { data } = useSWR(
    ['/api/posts', userId, 'published'],
    ([url, id, status]) => fetchPosts(url, id, status)
  );
  
  // Conditional fetching
  const { data: user } = useSWR('/api/user', fetcher);
  const { data: projects } = useSWR(
    // Only fetch when user is loaded
    user ? `/api/users/${user.id}/projects` : null,
    fetcher
  );
  
  return (/* ... */);
}

Mutation and Revalidation

import useSWR, { useSWRConfig } from 'swr';

function TodoList() {
  const { data: todos, mutate } = useSWR('/api/todos', fetcher);
  
  const addTodo = async (text) => {
    // Optimistic update
    const optimisticTodos = [...todos, { id: Date.now(), text, completed: false }];
    
    // Update local data immediately (optimistic)
    // Set revalidate to false to prevent immediate refetch
    mutate(optimisticTodos, false);
    
    // Send request to server
    await fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify({ text }),
    });
    
    // Revalidate to ensure data is correct
    mutate();
  };
  
  const toggleTodo = async (id) => {
    // Optimistic update
    mutate(
      todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t),
      false
    );
    
    await fetch(`/api/todos/${id}/toggle`, { method: 'PATCH' });
    mutate();
  };
  
  return (/* ... */);
}

// Global mutation
function LogoutButton() {
  const { mutate } = useSWRConfig();
  
  const logout = async () => {
    await fetch('/api/logout', { method: 'POST' });
    
    // Invalidate all keys matching pattern
    mutate(key => typeof key === 'string' && key.startsWith('/api/user'));
    
    // Or clear all cache
    mutate(() => true, undefined, { revalidate: false });
  };
  
  return <button onClick={logout}>Logout</button>;
}

Pagination

import useSWR from 'swr';

function PaginatedPosts() {
  const [page, setPage] = useState(1);
  
  const { data, isLoading } = useSWR(
    `/api/posts?page=${page}&limit=10`,
    fetcher,
    { keepPreviousData: true } // Keep showing old data while loading new
  );
  
  return (
    <div>
      {data?.posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
      
      <button 
        disabled={page === 1}
        onClick={() => setPage(p => p - 1)}
      >
        Previous
      </button>
      <span>Page {page}</span>
      <button 
        disabled={!data?.hasMore}
        onClick={() => setPage(p => p + 1)}
      >
        Next
      </button>
    </div>
  );
}

Infinite Loading

import useSWRInfinite from 'swr/infinite';

function InfinitePostList() {
  const getKey = (pageIndex, previousPageData) => {
    // Reached the end
    if (previousPageData && !previousPageData.hasMore) return null;
    
    // First page
    if (pageIndex === 0) return '/api/posts?page=1';
    
    // Next pages
    return `/api/posts?page=${pageIndex + 1}`;
  };
  
  const { data, size, setSize, isLoading, isValidating } = useSWRInfinite(
    getKey,
    fetcher
  );
  
  const posts = data ? data.flatMap(page => page.posts) : [];
  const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined');
  const isEmpty = data?.[0]?.posts?.length === 0;
  const isReachingEnd = isEmpty || (data && !data[data.length - 1]?.hasMore);
  
  return (
    <div>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
      
      <button
        disabled={isLoadingMore || isReachingEnd}
        onClick={() => setSize(size + 1)}
      >
        {isLoadingMore ? 'Loading...' : isReachingEnd ? 'No More' : 'Load More'}
      </button>
    </div>
  );
}

SWR vs TanStack Query

Feature SWR TanStack Query
Bundle size ~4KB ~12KB
API complexity Simpler More options
Mutations Manual with mutate() useMutation hook
DevTools Community Official
Best for Simple needs, Next.js Complex data requirements

💡 Best Practices

  • • Use SWRConfig for global fetcher and options
  • • Return null as key to conditionally disable fetching
  • • Use mutate() for optimistic updates
  • • Set appropriate revalidation intervals for your data
  • • Use keepPreviousData for smoother pagination
  • • Consider TanStack Query for complex mutation needs