SWR (stale-while-revalidate) provides elegant data fetching for React. Here's how to use it effectively.
Basic Setup#
1// lib/fetcher.ts
2export async function fetcher<T>(url: string): Promise<T> {
3 const response = await fetch(url);
4
5 if (!response.ok) {
6 const error = new Error('Fetch failed');
7 error.info = await response.json();
8 error.status = response.status;
9 throw error;
10 }
11
12 return response.json();
13}
14
15// Global configuration
16import { SWRConfig } from 'swr';
17
18function App({ children }) {
19 return (
20 <SWRConfig
21 value={{
22 fetcher,
23 revalidateOnFocus: true,
24 revalidateOnReconnect: true,
25 dedupingInterval: 2000,
26 errorRetryCount: 3,
27 }}
28 >
29 {children}
30 </SWRConfig>
31 );
32}Basic Data Fetching#
1import useSWR from 'swr';
2
3interface User {
4 id: string;
5 name: string;
6 email: string;
7}
8
9function useUser(id: string) {
10 const { data, error, isLoading, isValidating, mutate } = useSWR<User>(
11 id ? `/api/users/${id}` : null, // null key skips fetch
12 fetcher
13 );
14
15 return {
16 user: data,
17 isLoading,
18 isValidating,
19 isError: !!error,
20 error,
21 mutate,
22 };
23}
24
25// Component usage
26function UserProfile({ userId }: { userId: string }) {
27 const { user, isLoading, isError } = useUser(userId);
28
29 if (isLoading) return <Spinner />;
30 if (isError) return <Error />;
31
32 return (
33 <div>
34 <h1>{user.name}</h1>
35 <p>{user.email}</p>
36 </div>
37 );
38}Conditional Fetching#
1// Conditional key
2function useUserProfile(userId: string | null) {
3 return useSWR(
4 userId ? `/api/users/${userId}/profile` : null,
5 fetcher
6 );
7}
8
9// Dependent fetching
10function useUserOrders(userId: string) {
11 const { data: user } = useUser(userId);
12
13 // Fetch orders only after user is loaded
14 const { data: orders } = useSWR(
15 user ? `/api/users/${user.id}/orders` : null,
16 fetcher
17 );
18
19 return { user, orders };
20}
21
22// Multiple conditional keys
23function useData() {
24 const { data: user } = useSWR('/api/user', fetcher);
25 const { data: posts } = useSWR(
26 () => `/api/users/${user.id}/posts`,
27 fetcher
28 );
29 // Function key throws if user is undefined, skipping fetch
30}Mutation and Revalidation#
1import useSWRMutation from 'swr/mutation';
2
3// Mutation with trigger
4async function updateUser(url: string, { arg }: { arg: Partial<User> }) {
5 const response = await fetch(url, {
6 method: 'PATCH',
7 headers: { 'Content-Type': 'application/json' },
8 body: JSON.stringify(arg),
9 });
10
11 if (!response.ok) throw new Error('Update failed');
12 return response.json();
13}
14
15function useUpdateUser(userId: string) {
16 const { trigger, isMutating } = useSWRMutation(
17 `/api/users/${userId}`,
18 updateUser
19 );
20
21 return { updateUser: trigger, isUpdating: isMutating };
22}
23
24// Usage
25function EditProfile({ userId }: { userId: string }) {
26 const { user, mutate } = useUser(userId);
27 const { updateUser, isUpdating } = useUpdateUser(userId);
28
29 const handleSave = async (data: Partial<User>) => {
30 try {
31 // Optimistic update
32 await mutate(
33 updateUser(data),
34 {
35 optimisticData: { ...user, ...data },
36 rollbackOnError: true,
37 revalidate: false,
38 }
39 );
40 } catch (error) {
41 console.error('Update failed:', error);
42 }
43 };
44
45 return (
46 <form onSubmit={(e) => handleSave(getFormData(e))}>
47 {/* form fields */}
48 <button disabled={isUpdating}>
49 {isUpdating ? 'Saving...' : 'Save'}
50 </button>
51 </form>
52 );
53}Optimistic Updates#
1// Immediate UI update with rollback
2function TodoList() {
3 const { data: todos, mutate } = useSWR<Todo[]>('/api/todos', fetcher);
4
5 const addTodo = async (text: string) => {
6 const newTodo: Todo = {
7 id: Date.now().toString(), // Temporary ID
8 text,
9 completed: false,
10 };
11
12 // Optimistically update UI
13 mutate(
14 async (currentTodos) => {
15 const response = await fetch('/api/todos', {
16 method: 'POST',
17 body: JSON.stringify({ text }),
18 });
19 const createdTodo = await response.json();
20 return [...(currentTodos || []), createdTodo];
21 },
22 {
23 optimisticData: [...(todos || []), newTodo],
24 rollbackOnError: true,
25 populateCache: true,
26 revalidate: false,
27 }
28 );
29 };
30
31 const toggleTodo = async (id: string) => {
32 mutate(
33 async (currentTodos) => {
34 await fetch(`/api/todos/${id}/toggle`, { method: 'POST' });
35 return currentTodos?.map((todo) =>
36 todo.id === id ? { ...todo, completed: !todo.completed } : todo
37 );
38 },
39 {
40 optimisticData: todos?.map((todo) =>
41 todo.id === id ? { ...todo, completed: !todo.completed } : todo
42 ),
43 rollbackOnError: true,
44 }
45 );
46 };
47
48 return (
49 <ul>
50 {todos?.map((todo) => (
51 <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
52 {todo.text}
53 </li>
54 ))}
55 </ul>
56 );
57}Pagination#
1// Basic pagination
2function usePaginatedData(page: number) {
3 return useSWR(`/api/items?page=${page}`, fetcher, {
4 keepPreviousData: true, // Keep showing old data while fetching
5 });
6}
7
8// Infinite loading
9import useSWRInfinite from 'swr/infinite';
10
11function useInfiniteItems() {
12 const getKey = (pageIndex: number, previousPageData: Item[] | null) => {
13 // Return null to stop fetching
14 if (previousPageData && !previousPageData.length) return null;
15
16 return `/api/items?page=${pageIndex}`;
17 };
18
19 const { data, size, setSize, isValidating, isLoading } = useSWRInfinite(
20 getKey,
21 fetcher
22 );
23
24 const items = data ? data.flat() : [];
25 const isLoadingMore = isLoading || (size > 0 && !data?.[size - 1]);
26 const isEmpty = data?.[0]?.length === 0;
27 const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < 20);
28
29 return {
30 items,
31 isLoading,
32 isLoadingMore,
33 isReachingEnd,
34 loadMore: () => setSize(size + 1),
35 };
36}
37
38// Usage
39function InfiniteList() {
40 const { items, isLoadingMore, isReachingEnd, loadMore } = useInfiniteItems();
41
42 return (
43 <div>
44 {items.map((item) => (
45 <ItemCard key={item.id} item={item} />
46 ))}
47
48 <button onClick={loadMore} disabled={isLoadingMore || isReachingEnd}>
49 {isLoadingMore ? 'Loading...' : isReachingEnd ? 'No more' : 'Load more'}
50 </button>
51 </div>
52 );
53}Error Handling#
1// Global error handling
2<SWRConfig
3 value={{
4 onError: (error, key) => {
5 if (error.status !== 403 && error.status !== 404) {
6 // Report to error tracking
7 reportError(error);
8 }
9 },
10 onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
11 // Never retry on 404
12 if (error.status === 404) return;
13
14 // Only retry up to 3 times
15 if (retryCount >= 3) return;
16
17 // Retry after 5 seconds
18 setTimeout(() => revalidate({ retryCount }), 5000);
19 },
20 }}
21>
22 {children}
23</SWRConfig>
24
25// Per-hook error handling
26function useUserWithRetry(id: string) {
27 return useSWR(`/api/users/${id}`, fetcher, {
28 onError: (error) => {
29 toast.error(`Failed to load user: ${error.message}`);
30 },
31 errorRetryCount: 3,
32 errorRetryInterval: 1000,
33 });
34}Preloading#
1import { preload } from 'swr';
2
3// Preload on hover
4function UserLink({ userId }: { userId: string }) {
5 const handleMouseEnter = () => {
6 preload(`/api/users/${userId}`, fetcher);
7 };
8
9 return (
10 <Link href={`/users/${userId}`} onMouseEnter={handleMouseEnter}>
11 View User
12 </Link>
13 );
14}
15
16// Preload in route handlers
17// pages/users/[id].tsx
18export async function getServerSideProps({ params }) {
19 const user = await fetcher(`/api/users/${params.id}`);
20
21 return {
22 props: {
23 fallback: {
24 [`/api/users/${params.id}`]: user,
25 },
26 },
27 };
28}
29
30function UserPage({ fallback }) {
31 return (
32 <SWRConfig value={{ fallback }}>
33 <UserProfile />
34 </SWRConfig>
35 );
36}Best Practices#
Keys:
✓ Use consistent key patterns
✓ Include all dependencies in key
✓ Use null to skip fetching
✓ Consider key serialization
Caching:
✓ Set appropriate dedupingInterval
✓ Use keepPreviousData for pagination
✓ Configure revalidation strategies
✓ Use fallback for SSR
Mutations:
✓ Use optimistic updates for UX
✓ Always handle rollback
✓ Revalidate related data
✓ Show loading states
Conclusion#
SWR provides elegant, declarative data fetching with built-in caching and revalidation. Use optimistic updates for responsive UIs, infinite loading for lists, and proper error handling for resilience. The stale-while-revalidate strategy ensures users always see data quickly while keeping it fresh.