Setting up SWR for Data Caching in React Applications
SWR (stale-while-revalidate) is a library from Vercel for data fetching in React. The strategy is straightforward: first returns cached data (stale), simultaneously makes a request (revalidate), updates the cache. The user sees data instantly, it silently updates in the background.
More compact than React Query, smaller API, optimal for Next.js projects and cases where you don't need mutations with optimistic updates and infinite queries with pagination.
What's Included
Setting up global SWR config, custom fetcher, typed hooks, mutations, invalidation, offline mode, SSR with Next.js, DevTools.
Installation
npm install swr
Global Configuration
// main.tsx / _app.tsx
import { SWRConfig } from 'swr'
import { swrFetcher } from '@/lib/fetcher'
function App({ Component, pageProps }: AppProps) {
return (
<SWRConfig
value={{
fetcher: swrFetcher,
revalidateOnFocus: true,
revalidateOnReconnect: true,
shouldRetryOnError: true,
errorRetryCount: 3,
dedupingInterval: 2000,
onError: (error) => {
if (error.status === 401) {
authStore.logout()
}
},
}}
>
<Component {...pageProps} />
</SWRConfig>
)
}
Fetcher
// lib/fetcher.ts
import type { SWRConfiguration } from 'swr'
class ApiError extends Error {
constructor(public status: number, message: string) {
super(message)
this.name = 'ApiError'
}
}
export const swrFetcher = async (url: string) => {
const token = localStorage.getItem('token')
const res = await fetch(url, {
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
'Content-Type': 'application/json',
},
})
if (!res.ok) {
const body = await res.json().catch(() => ({}))
throw new ApiError(res.status, body.message ?? res.statusText)
}
return res.json()
}
Basic Hooks
// hooks/useUser.ts
import useSWR from 'swr'
export function useCurrentUser() {
const { data, error, isLoading, mutate } = useSWR<User>('/api/me')
return {
user: data,
isLoading,
isError: !!error,
error,
revalidate: mutate,
}
}
export function useUser(id: string | null) {
const { data, error, isLoading } = useSWR<User>(
id ? `/api/users/${id}` : null // null disables the request
)
return { user: data, isLoading, isError: !!error }
}
Hook with Parameters
// hooks/useProducts.ts
import useSWR from 'swr'
interface ProductFilters {
categoryId?: string
search?: string
page?: number
sort?: 'price' | 'name' | 'date'
}
export function useProducts(filters: ProductFilters) {
// key is URL with parameters, null disables the request
const params = new URLSearchParams(
Object.entries(filters)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, String(v)])
)
const { data, error, isLoading, isValidating } = useSWR<PaginatedResponse<Product>>(
`/api/products?${params.toString()}`
)
return {
products: data?.items ?? [],
total: data?.total ?? 0,
isLoading,
isValidating, // true during background revalidation
isError: !!error,
}
}
Mutations and Invalidation
import useSWR, { useSWRConfig } from 'swr'
function ProductEditor({ id }: { id: string }) {
const { mutate } = useSWRConfig()
const { data: product } = useSWR<Product>(`/api/products/${id}`)
async function handleUpdate(data: UpdateProductDto) {
// optimistic update
await mutate(
`/api/products/${id}`,
async (current: Product) => {
const updated = await api.patch<Product>(`/products/${id}`, data)
return updated
},
{
optimisticData: (current) => ({ ...current!, ...data }),
rollbackOnError: true,
revalidate: false, // don't re-request after mutation — we already have fresh data
}
)
// invalidate product list
await mutate((key) => typeof key === 'string' && key.startsWith('/api/products?'))
}
// ...
}
useSWRMutation — Explicit Mutations
import useSWRMutation from 'swr/mutation'
async function createProduct(url: string, { arg }: { arg: CreateProductDto }) {
return api.post<Product>(url, arg)
}
function CreateProductForm() {
const { trigger, isMutating, error } = useSWRMutation('/api/products', createProduct)
const { mutate } = useSWRConfig()
async function handleSubmit(data: CreateProductDto) {
const newProduct = await trigger(data)
// invalidate all lists
await mutate((key) => typeof key === 'string' && key.includes('/api/products'))
}
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
<button disabled={isMutating}>
{isMutating ? 'Creating...' : 'Create'}
</button>
{error && <p className="error">{error.message}</p>}
</form>
)
}
Infinite Loading
import useSWRInfinite from 'swr/infinite'
const PAGE_SIZE = 20
function getKey(pageIndex: number, previousPageData: PaginatedResponse<Product> | null) {
if (previousPageData && !previousPageData.hasNextPage) return null
return `/api/products?page=${pageIndex + 1}&pageSize=${PAGE_SIZE}`
}
export function useInfiniteProducts() {
const { data, size, setSize, isLoading, isValidating } = useSWRInfinite<
PaginatedResponse<Product>
>(getKey)
const products = data?.flatMap((page) => page.items) ?? []
const isLoadingMore = isLoading || (size > 0 && data && data[size - 1] === undefined)
const hasMore = data ? data[data.length - 1]?.hasNextPage : true
return {
products,
isLoading,
isLoadingMore,
hasMore,
loadMore: () => setSize(size + 1),
}
}
Conditional Fetching — Dependent Requests
function OrderDetails({ orderId }: { orderId: string }) {
const { user } = useCurrentUser()
// request only executes after user is fetched
const { data: order } = useSWR<Order>(
user ? `/api/orders/${orderId}` : null
)
// another dependent request
const { data: products } = useSWR<Product[]>(
order?.productIds ? `/api/products?ids=${order.productIds.join(',')}` : null
)
// ...
}
SSR with Next.js (Pages Router)
// pages/products/[id].tsx
import { unstable_serialize } from 'swr'
import { SWRConfig } from 'swr'
export async function getServerSideProps({ params }: GetServerSidePropsContext) {
const product = await fetchProductServer(params!.id as string)
return {
props: {
fallback: {
[unstable_serialize(`/api/products/${params!.id}`)]: product,
},
},
}
}
export default function ProductPage({ fallback }: { fallback: Record<string, Product> }) {
return (
<SWRConfig value={{ fallback }}>
<ProductDetails />
</SWRConfig>
)
}
function ProductDetails() {
const { id } = useRouter().query
const { data } = useSWR<Product>(`/api/products/${id}`)
// data is immediately available from fallback, no loading
return <div>{data?.name}</div>
}
Offline and Revalidate on Focus
// Globally disable revalidateOnFocus for rarely changing data
const { data } = useSWR('/api/config', fetcher, {
revalidateOnFocus: false,
revalidateIfStale: false, // don't re-request if data is still fresh
})
// Manual invalidation on event
window.addEventListener('focus', () => {
mutate('/api/notifications') // update only notifications
})
Hook Structure
src/hooks/
useCurrentUser.ts
useProducts.ts
useProduct.ts
useOrders.ts
useNotifications.ts
What We Do
Set up global SWRConfig with custom fetcher, design hooks for all API endpoints, implement optimistic mutations for forms, configure invalidation on changes, add SSR-prefetch if needed.
Timeline: 2–3 days.







