SWR
React hooks for data fetching with stale-while-revalidate strategy by Vercel
SWR - Stale-While-Revalidate
SWR is a React hooks library for data fetching created by Vercel. The name comes from the HTTP cache invalidation strategy "stale-while-revalidate" - it returns cached (stale) data first, then fetches fresh data in the background. It's lightweight, fast, and integrates seamlessly with Next.js.
Key Features
- Lightweight — Only ~4KB gzipped
- Fast — Instant UI with cached data
- Revalidation — Automatic background updates
- Focus Revalidation — Refetch when tab regains focus
- Next.js Integration — Works great with Next.js
Installation
npm install swr
Basic Usage
import useSWR from 'swr';
// Fetcher function
const fetcher = (url) => fetch(url).then(res => res.json());
function Profile() {
const { data, error, isLoading, mutate } = useSWR('/api/user', fetcher);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Failed to load</div>;
return (
<div>
<h1>Hello, {data.name}!</h1>
<button onClick={() => mutate()}>Refresh</button>
</div>
);
}
Global Configuration
import { SWRConfig } from 'swr';
// Global fetcher and options
function App() {
return (
<SWRConfig
value={{
fetcher: (url) => fetch(url).then(res => res.json()),
refreshInterval: 3000, // Refresh every 3s
revalidateOnFocus: true, // Refetch on window focus
dedupingInterval: 2000, // Dedupe requests within 2s
onError: (error) => {
console.error('SWR Error:', error);
},
}}
>
<MyApp />
</SWRConfig>
);
}
// Now components don't need to specify fetcher
function UserProfile() {
const { data } = useSWR('/api/user');
return <div>{data?.name}</div>;
}
Dynamic Keys
import useSWR from 'swr';
function UserPosts({ userId }) {
// Key can be string, array, or null
const { data: posts } = useSWR(
userId ? `/api/users/${userId}/posts` : null,
fetcher
);
// Array keys for complex fetchers
const { data } = useSWR(
['/api/posts', userId, 'published'],
([url, id, status]) => fetchPosts(url, id, status)
);
// Conditional fetching
const { data: user } = useSWR('/api/user', fetcher);
const { data: projects } = useSWR(
// Only fetch when user is loaded
user ? `/api/users/${user.id}/projects` : null,
fetcher
);
return (/* ... */);
}
Mutation and Revalidation
import useSWR, { useSWRConfig } from 'swr';
function TodoList() {
const { data: todos, mutate } = useSWR('/api/todos', fetcher);
const addTodo = async (text) => {
// Optimistic update
const optimisticTodos = [...todos, { id: Date.now(), text, completed: false }];
// Update local data immediately (optimistic)
// Set revalidate to false to prevent immediate refetch
mutate(optimisticTodos, false);
// Send request to server
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text }),
});
// Revalidate to ensure data is correct
mutate();
};
const toggleTodo = async (id) => {
// Optimistic update
mutate(
todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t),
false
);
await fetch(`/api/todos/${id}/toggle`, { method: 'PATCH' });
mutate();
};
return (/* ... */);
}
// Global mutation
function LogoutButton() {
const { mutate } = useSWRConfig();
const logout = async () => {
await fetch('/api/logout', { method: 'POST' });
// Invalidate all keys matching pattern
mutate(key => typeof key === 'string' && key.startsWith('/api/user'));
// Or clear all cache
mutate(() => true, undefined, { revalidate: false });
};
return <button onClick={logout}>Logout</button>;
}
Pagination
import useSWR from 'swr';
function PaginatedPosts() {
const [page, setPage] = useState(1);
const { data, isLoading } = useSWR(
`/api/posts?page=${page}&limit=10`,
fetcher,
{ keepPreviousData: true } // Keep showing old data while loading new
);
return (
<div>
{data?.posts.map(post => (
<PostCard key={post.id} post={post} />
))}
<button
disabled={page === 1}
onClick={() => setPage(p => p - 1)}
>
Previous
</button>
<span>Page {page}</span>
<button
disabled={!data?.hasMore}
onClick={() => setPage(p => p + 1)}
>
Next
</button>
</div>
);
}
Infinite Loading
import useSWRInfinite from 'swr/infinite';
function InfinitePostList() {
const getKey = (pageIndex, previousPageData) => {
// Reached the end
if (previousPageData && !previousPageData.hasMore) return null;
// First page
if (pageIndex === 0) return '/api/posts?page=1';
// Next pages
return `/api/posts?page=${pageIndex + 1}`;
};
const { data, size, setSize, isLoading, isValidating } = useSWRInfinite(
getKey,
fetcher
);
const posts = data ? data.flatMap(page => page.posts) : [];
const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined');
const isEmpty = data?.[0]?.posts?.length === 0;
const isReachingEnd = isEmpty || (data && !data[data.length - 1]?.hasMore);
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
<button
disabled={isLoadingMore || isReachingEnd}
onClick={() => setSize(size + 1)}
>
{isLoadingMore ? 'Loading...' : isReachingEnd ? 'No More' : 'Load More'}
</button>
</div>
);
}
SWR vs TanStack Query
| Feature | SWR | TanStack Query |
|---|---|---|
| Bundle size | ~4KB | ~12KB |
| API complexity | Simpler | More options |
| Mutations | Manual with mutate() | useMutation hook |
| DevTools | Community | Official |
| Best for | Simple needs, Next.js | Complex data requirements |
💡 Best Practices
- • Use SWRConfig for global fetcher and options
- • Return null as key to conditionally disable fetching
- • Use mutate() for optimistic updates
- • Set appropriate revalidation intervals for your data
- • Use keepPreviousData for smoother pagination
- • Consider TanStack Query for complex mutation needs