Back to Blog
SWRReactData FetchingHooks

SWR for Data Fetching in React

Master SWR for React data fetching. From basic usage to revalidation strategies to optimistic updates.

B
Bootspring Team
Engineering
November 4, 2021
6 min read

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.

Share this article

Help spread the word about Bootspring