SWR Complete Guide | React Data Fetching by Vercel

SWR Complete Guide | React Data Fetching by Vercel

이 글의 핵심

SWR (stale-while-revalidate) is a React Hooks library for data fetching by Vercel. It provides caching, revalidation, focus tracking, and real-time updates out of the box.

Introduction

SWR is a React Hooks library for data fetching created by Vercel (the team behind Next.js). The name comes from stale-while-revalidate, an HTTP caching strategy that returns cached data immediately while fetching fresh data in the background.

The Problem

Traditional data fetching:

useEffect(() => {
  setLoading(true);
  fetch('/api/user')
    .then(res => res.json())
    .then(data => {
      setData(data);
      setLoading(false);
    });
}, []);

Problems:

  • Manual loading state
  • No caching
  • No revalidation
  • Boilerplate code

The Solution

With SWR:

const { data, error, isLoading } = useSWR('/api/user', fetcher);

1. Installation

npm install swr

2. Basic Usage

Simple Fetching

import useSWR from 'swr';

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

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

With TypeScript

interface User {
  id: number;
  name: string;
  email: string;
}

function Profile() {
  const { data, error, isLoading } = useSWR<User>(
    '/api/user',
    fetcher
  );
  
  return <div>{data?.name}</div>;
}

3. Global Configuration

// app/layout.tsx (Next.js)
import { SWRConfig } from 'swr';

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <SWRConfig
      value={{
        fetcher: (url: string) => fetch(url).then(res => res.json()),
        revalidateOnFocus: false,
        revalidateOnReconnect: true,
      }}
    >
      {children}
    </SWRConfig>
  );
}

Now you don’t need to pass fetcher every time:

const { data } = useSWR('/api/user'); // Fetcher is global

4. Mutations

Basic Mutation

import useSWR, { mutate } from 'swr';

function UpdateProfile() {
  const { data } = useSWR('/api/user', fetcher);
  
  async function updateUser(newName: string) {
    // Update API
    await fetch('/api/user', {
      method: 'PATCH',
      body: JSON.stringify({ name: newName }),
    });
    
    // Revalidate
    mutate('/api/user');
  }
  
  return <button onClick={() => updateUser('New Name')}>Update</button>;
}

Optimistic Updates

import useSWR, { mutate } from 'swr';

function TodoList() {
  const { data: todos } = useSWR<Todo[]>('/api/todos', fetcher);
  
  async function addTodo(title: string) {
    const newTodo = { id: Date.now(), title, completed: false };
    
    // Optimistic update
    mutate(
      '/api/todos',
      async (currentTodos) => {
        // Update UI immediately
        const updatedTodos = [...(currentTodos || []), newTodo];
        
        // Send request
        await fetch('/api/todos', {
          method: 'POST',
          body: JSON.stringify(newTodo),
        });
        
        // Return optimistic data
        return updatedTodos;
      },
      {
        optimisticData: [...(todos || []), newTodo],
        rollbackOnError: true,
      }
    );
  }
  
  return (
    <div>
      {todos?.map(todo => <div key={todo.id}>{todo.title}</div>)}
    </div>
  );
}

Bound Mutate

import useSWR from 'swr';

function Profile() {
  const { data, mutate } = useSWR('/api/user', fetcher);
  
  async function updateUser() {
    // Update with bound mutate (no need to pass key)
    await mutate(async (user) => {
      await fetch('/api/user', { method: 'PATCH', body: {...} });
      return { ...user, name: 'Updated' };
    });
  }
  
  return <div>{data?.name}</div>;
}

5. Conditional Fetching

function UserProfile({ userId }: { userId?: number }) {
  // Only fetch if userId exists
  const { data } = useSWR(
    userId ? `/api/users/${userId}` : null,
    fetcher
  );
  
  return <div>{data?.name}</div>;
}

6. Dependent Requests

function UserPosts({ userId }: { userId: number }) {
  // First request
  const { data: user } = useSWR(`/api/users/${userId}`, fetcher);
  
  // Second request depends on first
  const { data: posts } = useSWR(
    user ? `/api/posts?userId=${user.id}` : null,
    fetcher
  );
  
  return <div>{posts?.length} posts</div>;
}

7. Pagination

function UserList() {
  const [page, setPage] = useState(1);
  const { data, error, isLoading } = useSWR(
    `/api/users?page=${page}&limit=20`,
    fetcher
  );
  
  return (
    <div>
      {data?.users.map((user: any) => (
        <div key={user.id}>{user.name}</div>
      ))}
      
      <button onClick={() => setPage(page - 1)} disabled={page === 1}>
        Previous
      </button>
      <button onClick={() => setPage(page + 1)}>Next</button>
    </div>
  );
}

8. Infinite Loading

import useSWRInfinite from 'swr/infinite';

function InfiniteList() {
  const getKey = (pageIndex: number, previousPageData: any) => {
    // Reached the end
    if (previousPageData && !previousPageData.hasMore) return null;
    
    // First page
    return `/api/users?page=${pageIndex + 1}&limit=20`;
  };
  
  const { data, size, setSize, isLoading } = useSWRInfinite(
    getKey,
    fetcher
  );
  
  const users = data ? data.flatMap(page => page.users) : [];
  const hasMore = data?.[data.length - 1]?.hasMore;
  
  return (
    <div>
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
      
      {hasMore && (
        <button onClick={() => setSize(size + 1)}>Load More</button>
      )}
    </div>
  );
}

9. Real-time Updates

Auto Revalidation

const { data } = useSWR('/api/data', fetcher, {
  refreshInterval: 3000, // Refresh every 3 seconds
  refreshWhenHidden: false, // Pause when tab hidden
  refreshWhenOffline: false, // Pause when offline
});

Manual Revalidation

import { useSWRConfig } from 'swr';

function RefreshButton() {
  const { mutate } = useSWRConfig();
  
  return (
    <button onClick={() => mutate('/api/user')}>
      Refresh
    </button>
  );
}

10. Error Handling

Retry on Error

const { data, error } = useSWR('/api/user', fetcher, {
  onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
    // Never retry on 404
    if (error.status === 404) return;
    
    // Only retry 3 times
    if (retryCount >= 3) return;
    
    // Retry after 5 seconds
    setTimeout(() => revalidate({ retryCount }), 5000);
  },
});

Error Handling

const { data, error } = useSWR('/api/user', fetcher, {
  onError: (error, key) => {
    console.error('SWR error:', error);
    toast.error('Failed to fetch data');
  },
  onSuccess: (data, key, config) => {
    console.log('Data loaded:', data);
  },
});

11. Prefetching

import { mutate } from 'swr';

function UserList({ users }: { users: User[] }) {
  const prefetch = (userId: number) => {
    // Prefetch user data
    mutate(
      `/api/users/${userId}`,
      fetch(`/api/users/${userId}`).then(res => res.json())
    );
  };
  
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id} onMouseEnter={() => prefetch(user.id)}>
          <Link to={`/users/${user.id}`}>{user.name}</Link>
        </li>
      ))}
    </ul>
  );
}

12. Middleware

import useSWR from 'swr';

// Logging middleware
function logger(useSWRNext: any) {
  return (key: any, fetcher: any, config: any) => {
    const swr = useSWRNext(key, fetcher, config);
    
    useEffect(() => {
      console.log('SWR Request:', key);
    }, [key]);
    
    return swr;
  };
}

// Use middleware
const { data } = useSWR('/api/user', fetcher, { use: [logger] });

13. Best Practices

1. Create Custom Hooks

// hooks/useUser.ts
export function useUser(id: number) {
  return useSWR<User>(
    id ? `/api/users/${id}` : null,
    fetcher,
    {
      revalidateOnFocus: false,
      dedupingInterval: 2000,
    }
  );
}

// Usage
const { data: user } = useUser(123);

2. Handle Loading States

function Component() {
  const { data, error, isLoading, isValidating } = useSWR(key, fetcher);
  
  // First load
  if (isLoading) return <Skeleton />;
  
  // Error state
  if (error) return <ErrorMessage error={error} />;
  
  // Background revalidation indicator
  return (
    <div>
      {isValidating && <RefreshIcon className="animate-spin" />}
      {data && <DataDisplay data={data} />}
    </div>
  );
}

3. Cache Management

import { useSWRConfig } from 'swr';

function CacheManager() {
  const { cache, mutate } = useSWRConfig();
  
  const clearCache = () => {
    // Clear specific key
    mutate('/api/user', undefined, { revalidate: false });
    
    // Clear all cache
    if (cache instanceof Map) {
      cache.clear();
    }
  };
  
  return <button onClick={clearCache}>Clear Cache</button>;
}

14. Next.js Integration

API Routes

// app/api/users/route.ts (Next.js 13+)
export async function GET() {
  const users = await prisma.user.findMany();
  return Response.json({ users });
}

Client Component

'use client';

import useSWR from 'swr';

export function UserList() {
  const { data } = useSWR('/api/users', fetcher);
  
  return (
    <ul>
      {data?.users.map((user: any) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

15. Performance Tips

Deduplication

// Multiple components can use the same key
// SWR makes only ONE request
function ComponentA() {
  const { data } = useSWR('/api/user', fetcher);
}

function ComponentB() {
  const { data } = useSWR('/api/user', fetcher); // Uses same cache
}

Focus Revalidation

const { data } = useSWR('/api/data', fetcher, {
  // Revalidate when tab/window gains focus
  revalidateOnFocus: true,
  
  // Minimum interval between revalidations
  focusThrottleInterval: 5000, // 5 seconds
});

Summary

SWR simplifies React data fetching:

  • Automatic caching with stale-while-revalidate
  • Smart revalidation on focus, reconnect, interval
  • Optimistic updates for instant UI feedback
  • Real-time support with polling and WebSocket
  • Lightweight - only 4KB gzipped

Key Takeaways:

  1. Returns cached data first, fetches fresh data in background
  2. Use mutate for optimistic updates
  3. Configure globally with SWRConfig
  4. Create custom hooks for reusability
  5. Perfect for Next.js and Vercel deployments

Next Steps:

Resources: