SWR implements the stale-while-revalidate strategy — useSWR(key, fetcher) returns { data, error, isLoading }. The key is the cache key and URL. middleware objects wrap requests. SWRConfig sets global defaults with fetcher, onError, and revalidateOnFocus. mutate(key, data, { revalidate: false }) updates the cache optimistically. useSWRMutation(key, fetcher) returns { trigger, isMutating } for POST/PUT/DELETE. useSWRInfinite(getKey, fetcher) handles paginated lists with size and setSize. Conditional fetching uses null as key — useSWR(userId ? \/users/${userId}` : null, fetcher). swr/immutablefor data that never changes.preload(“/api/user”, fetcher)warms the cache before component mount.errorRetryCount: 3andonErrorRetrycustomize retry behavior. TypeScript:useSWR
CLAUDE.md for SWR
## SWR Stack
- Version: swr >= 2.2
- Global: <SWRConfig value={{ fetcher: (url) => fetch(url).then(r => { if (!r.ok) throw r; return r.json() }), revalidateOnFocus: true }}>
- Query: const { data, error, isLoading } = useSWR<User>("/api/user")
- Mutate: const { mutate } = useSWR(...); mutate(optimisticData, { revalidate: false })
- Global mutate: mutate("/api/users") — refetch without hook ref
- Mutation: const { trigger, isMutating } = useSWRMutation("/api/users", async (url, { arg }) => poster(url, arg))
- Infinite: const { data, size, setSize } = useSWRInfinite((index) => `/api/posts?page=${index + 1}`)
- Conditional: useSWR(userId ? `/api/users/${userId}` : null, fetcher)
Global Configuration
// providers/SWRProvider.tsx — global SWR config
"use client"
import { SWRConfig, type SWRConfiguration } from "swr"
import { useCallback } from "react"
// Generic authenticated fetcher
async function apiFetcher<T>(url: string): Promise<T> {
const res = await fetch(url, {
credentials: "include",
headers: { "Content-Type": "application/json" },
})
if (!res.ok) {
const error = await res.json().catch(() => ({ message: "An error occurred" }))
throw Object.assign(new Error(error.message ?? `HTTP ${res.status}`), {
status: res.status,
info: error,
})
}
return res.json()
}
const swrConfig: SWRConfiguration = {
fetcher: apiFetcher,
revalidateOnFocus: true,
revalidateOnReconnect: true,
errorRetryCount: 3,
errorRetryInterval: 1000,
dedupingInterval: 2000,
onError: (error) => {
// Log errors but don't toast 401 (handled by redirect)
if (error.status !== 401) {
console.error("[SWR]", error.message)
}
},
}
export function SWRProvider({ children }: { children: React.ReactNode }) {
return (
<SWRConfig value={swrConfig}>
{children}
</SWRConfig>
)
}
Typed Data Hooks
// hooks/useUser.ts — typed SWR hooks with mutations
import useSWR from "swr"
import useSWRMutation from "swr/mutation"
export type User = {
id: string
name: string
email: string
role: "user" | "admin"
avatarUrl: string | null
plan: "free" | "pro"
}
export type UpdateUserInput = Partial<Pick<User, "name" | "avatarUrl">>
// Read current user
export function useCurrentUser() {
const { data, error, isLoading, mutate } = useSWR<User>("/api/auth/me")
return {
user: data,
isLoading,
isError: !!error,
isUnauthorized: error?.status === 401,
mutate,
}
}
// Helper: POST/PUT/PATCH/DELETE
async function apiMutate<T>(url: string, { arg }: { arg: { method: string; body?: unknown } }): Promise<T> {
const res = await fetch(url, {
method: arg.method,
credentials: "include",
headers: { "Content-Type": "application/json" },
body: arg.body ? JSON.stringify(arg.body) : undefined,
})
if (!res.ok) {
const error = await res.json().catch(() => ({}))
throw Object.assign(new Error(error.message ?? "Request failed"), { status: res.status })
}
return res.status === 204 ? null as T : res.json()
}
// Update user with optimistic update
export function useUpdateUser() {
const { mutate: mutateUser } = useSWR<User>("/api/auth/me")
const { trigger, isMutating } = useSWRMutation(
"/api/auth/me",
(url, { arg }: { arg: UpdateUserInput }) =>
apiMutate<User>(url, { arg: { method: "PUT", body: arg } }),
{
onSuccess: (updated) => {
// Update cache with server response
mutateUser(updated, { revalidate: false })
},
},
)
const updateUser = async (input: UpdateUserInput) => {
// Optimistic update
await mutateUser(
async (current) => {
if (!current) return current
return { ...current, ...input }
},
{
optimisticData: (current) => current ? { ...current, ...input } : undefined,
revalidate: false,
rollbackOnError: true,
},
)
}
return { trigger, isMutating, updateUser }
}
Infinite Scroll Feed
// components/feed/InfiniteFeed.tsx — useSWRInfinite pagination
"use client"
import useSWRInfinite from "swr/infinite"
import { useIntersection } from "@/hooks/useIntersection"
import { useEffect, useRef } from "react"
type Post = {
id: string
title: string
excerpt: string
author: { name: string; avatarUrl: string }
createdAt: string
}
type PostsPage = {
items: Post[]
nextCursor: string | null
}
const PAGE_SIZE = 20
export function InfiniteFeed() {
const sentinelRef = useRef<HTMLDivElement>(null)
const isVisible = useIntersection(sentinelRef)
const { data, error, isLoading, size, setSize, isValidating } = useSWRInfinite<PostsPage>(
(pageIndex, previousPageData) => {
// Reached end
if (previousPageData && !previousPageData.nextCursor) return null
// First page
if (pageIndex === 0) return `/api/posts?limit=${PAGE_SIZE}`
// Subsequent pages — cursor-based
return `/api/posts?limit=${PAGE_SIZE}&cursor=${previousPageData!.nextCursor}`
},
{ revalidateFirstPage: false, parallel: false },
)
const posts = data?.flatMap(page => page.items) ?? []
const hasMore = data?.[data.length - 1]?.nextCursor != null
const isLoadingMore = size > 1 && data && typeof data[size - 1] === "undefined"
// Auto-load on scroll
useEffect(() => {
if (isVisible && hasMore && !isValidating) {
setSize(s => s + 1)
}
}, [isVisible, hasMore, isValidating, setSize])
if (isLoading) {
return (
<div className="space-y-4">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="rounded-xl border p-4 space-y-3 animate-pulse">
<div className="flex gap-3">
<div className="size-10 rounded-full bg-muted" />
<div className="space-y-2 flex-1">
<div className="h-3 bg-muted rounded w-1/3" />
<div className="h-3 bg-muted rounded w-1/4" />
</div>
</div>
<div className="h-4 bg-muted rounded w-3/4" />
<div className="h-4 bg-muted rounded w-1/2" />
</div>
))}
</div>
)
}
if (error) return <p className="text-red-500 text-sm">Failed to load posts</p>
return (
<div className="space-y-4">
{posts.map(post => (
<article key={post.id} className="rounded-xl border p-4 space-y-3">
<div className="flex items-center gap-3">
<img src={post.author.avatarUrl} alt="" className="size-10 rounded-full object-cover" />
<div>
<p className="font-medium text-sm">{post.author.name}</p>
<p className="text-xs text-muted-foreground">
{new Date(post.createdAt).toLocaleDateString()}
</p>
</div>
</div>
<h2 className="font-semibold">{post.title}</h2>
<p className="text-muted-foreground text-sm line-clamp-3">{post.excerpt}</p>
</article>
))}
{/* Intersection sentinel */}
<div ref={sentinelRef} className="h-4" />
{isLoadingMore && (
<div className="flex justify-center py-4">
<div className="size-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div>
)}
{!hasMore && posts.length > 0 && (
<p className="text-center text-sm text-muted-foreground py-4">You've reached the end</p>
)}
</div>
)
}
Conditional and Dependent Queries
// hooks/useProductWithReviews.ts — dependent SWR queries
import useSWR from "swr"
type Product = { id: string; name: string; categoryId: string }
type Review = { id: string; rating: number; comment: string }
type Category = { id: string; name: string; slug: string }
export function useProductWithReviews(productId: string | undefined) {
// Main resource
const { data: product, isLoading: productLoading } = useSWR<Product>(
productId ? `/api/products/${productId}` : null,
)
// Dependent — only fetch after product loads
const { data: reviews, isLoading: reviewsLoading } = useSWR<Review[]>(
product ? `/api/products/${product.id}/reviews` : null,
)
// Parallel dependent — fetch category alongside reviews
const { data: category } = useSWR<Category>(
product?.categoryId ? `/api/categories/${product.categoryId}` : null,
)
return {
product,
reviews,
category,
isLoading: productLoading || reviewsLoading,
}
}
For the TanStack Query (React Query) alternative when more advanced cache control, background synchronization, mutation options, and QueryClient configuration are needed — TanStack Query has a richer API for mutation states, retry policies, and optimistic updates than SWR, but SWR is simpler and has a smaller bundle, see the TanStack Query guide. For the RTK Query alternative when you already use Redux and want server-state caching integrated in the same store with createApi endpoint definitions — RTK Query is better when your app uses Redux for client state and you want a single store for everything, see the RTK Query guide. The Claude Skills 360 bundle includes SWR skill sets covering hooks, mutations, and infinite pagination. Start with the free tier to try SWR data fetching generation.