SWR Stale-While-Revalidate
Introduction
SWR is a lightweight, backend-agnostic React Hooks library for data fetching, created by Vercel. It aims to provide a fast and reactive user experience by keeping data synchronized and fresh without manual state management.
Core Concept: Stale-While-Revalidate
The name “SWR” is derived from stale-while-revalidate, a cache invalidation strategy popularized by HTTP RFC 5861. Its operational logic follows three main steps:
- Stale: It immediately returns data from the cache (if available) to show the user something right away.
- Revalidate: In the background, it sends a fetch request to the server to check for updates.
- Update: Once the new data arrives, it automatically updates the UI with the fresh content.
Key Features
According to the SWR documentation, the library simplifies complex data-fetching logic through several built-in features: Automatic Revalidation: It can refetch data when a user refocuses on a tab or when the network recovers. Caching & Deduplication: Prevents redundant network requests by sharing the same request across different components. Optimistic UI: Allows you to update the UI locally first and then sync with the server, making the app feel instantaneous. Advanced Utilities: Includes native support for pagination, scroll position recovery, and dependent fetching (where one request depends on the result of another).
Why Use It?
While standard tools like fetch or Axios only handle the request-response cycle, SWR acts as a management layer. It eliminates the need for boilerplate useEffect hooks and manual loading/error state tracking, providing these values directly from the useSWR hook.
To use SWR, you typically need three things: a key (the API URL), a fetcher function, and the useSWR hook itself.
1. The Basic Implementation
In this example, we fetch a list of users. SWR handles the loading and error states automatically, so you don’t need to manage them with useState or useEffect.
import useSWR from 'swr'
// 1. Define a fetcher function.
// It can be any async function (using fetch, Axios, etc.)
const fetcher = (url) => fetch(url).then((res) => res.json())
function UserProfile() {
// 2. Use the hook.
// It returns data, error, and an isLoading state.
const { data, error, isLoading } = useSWR('https://example.com', fetcher)
// 3. Handle different states in your UI
if (error) return <div>Failed to load user.</div>
if (isLoading) return <div>Loading...</div>
// Render the data
return <div>Hello, {data.name}!</div>
}
2. How it Works
The Key: The first argument (‘https://example.com ’) is the unique key of the request. If multiple components use this same key, SWR will only send one network request and share the result across all of them. The Fetcher: This is any function that returns a Promise. You can easily swap the native fetch for Axios if you prefer. Return Values:
- data: The response from the fetcher.
- error: Any error thrown by the fetcher. isLoading: A boolean that is true while the request is in flight and there is no cached data yet
3. Reusable Hooks (Best Practice)
For a cleaner architecture, you can wrap your SWR logic into a custom hook. This allows you to reuse the data-fetching logic across multiple components without repeating the URL or fetcher.
function useUser(id) {
const { data, error, isLoading } = useSWR(`/api/user/${id}`, fetcher)
return {
user: data,
isLoading,
isError: error
}
}
Using Mutate
In SWR, the mutate function allows you to manually update the cached data or trigger a re-fetch. This is essential for keeping the UI in sync after a user action, like adding or editing an item.
There are two ways to use it:
1. Bound Mutate (Recommended for Local Updates)
The mutate function returned by the useSWR hook is “bound” to that specific key. You can use it to refresh data or perform “Optimistic UI” updates—where you update the screen before the server even responds.
const { data, mutate } = useSWR('/api/user', fetcher);
const handleUpdate = async () => {
const newName = "Updated Name";
// 1. Update the local UI immediately (Optimistic)
// 2. Send the request to the server
// 3. Revalidate to ensure local data matches the server
mutate(updateUser(newName), {
optimisticData: { ...data, name: newName },
rollbackOnError: true,
});
};
- optimisticData: The data to show the user immediately.
- rollbackOnError: If the server request fails, SWR automatically reverts the UI to the previous state.
2. Global Mutate (Update from Anywhere)
If you need to trigger an update for a component that isn’t currently using the useSWR hook, you can use the global mutate API by importing it directly from the SWR package.
import { mutate } from 'swr';
function LogoutButton() {
return (
<button onClick={() => {
// Clear auth tokens/cookies...
// Then tell all components using '/api/user' to re-fetch
mutate('/api/user');
}}>
Logout
</button>
);
}Summary of Behaviors
- mutate(): Calling it with no arguments simply forces a background re-fetch (revalidation).
- mutate(data): Updates the local cache with new data and then revalidates.
- mutate(data, false): Updates the local cache but skips revalidation (useful if you are certain your local data is 100% correct)
useSWRMutation
Introduced in SWR 2.0, the useSWRMutation hook is designed for remote mutations (POST, PUT, DELETE) that are triggered manually. Unlike useSWR, it does not fetch data automatically when a component mounts.
Basic Implementation
To use it, you define a mutation fetcher that accepts an additional arg object. This arg contains the data you pass when calling the mutation.
import useSWRMutation from 'swr/mutation'
// The fetcher receives the key (url) and an options object containing { arg }
async function sendRequest(url, { arg }) {
return fetch(url, {
method: 'POST',
body: JSON.stringify(arg)
}).then(res => res.json())
}
function App() {
// trigger: function to start the mutation
// isMutating: boolean to track the request status
const { trigger, isMutating } = useSWRMutation('/api/user', sendRequest)
return (
<button
disabled={isMutating}
onClick={async () => {
try {
// Pass the data you want to send as the argument
const result = await trigger({ username: 'johndoe' })
console.log('Success:', result)
} catch (e) {
console.error('Failed:', e)
}
}}
>
{isMutating ? 'Creating...' : 'Create User'}
</button>
)
}
Why Use useSWRMutation?
- Manual Control: It only executes when you call the trigger function, making it perfect for form submissions or button clicks.
- Automatic Revalidation: When the mutation finishes, SWR automatically revalidates any useSWR hooks that use the same key. This keeps your UI in sync without needing to manually call mutate().
- Shared State: It shares the same cache as useSWR, helping to avoid race conditions between simultaneous data fetching and updates.
- Status Tracking: It provides built-in isMutating, data, and error states specifically for that mutation instance.
Handling Success and Errors
You can handle results either through the await trigger() response or by using the hook’s returned states:
- data: The result of the successful mutation.
- error: Any error that occurred during the request.
- reset(): A function to clear the current data and error states
SWRProvider
Pros
1. Centralized SWR Configuration
- You can define a global fetcher, error handling, revalidation intervals, deduplication, and cache policies in one place.
- Example:
<SWRConfig
value={{
fetcher: (url) => fetch(url).then(res => res.json()),
revalidateOnFocus: true,
}}
>
<App />
</SWRConfig>- All nested SWR hooks automatically inherit this, reducing repetitive setup.
2. Consistent Caching Across App
- Root-level provider ensures all components share the same cache.
- Prevents redundant network requests when multiple components fetch the same resource.
- Useful for global data (user info, settings, navigation data, etc.).
3. Simplified Global State Management
- SWR can act as a lightweight global state for remote data.
- For example, user info fetched in one component can be accessed everywhere without prop-drilling.
4. Easy Revalidation Policies
- You can configure things like refreshInterval, revalidateOnFocus, or revalidateOnReconnect globally.
- Makes behavior consistent across your app, which is especially handy in PWAs or offline-capable apps.
5. Potential for DevTools and Middleware
- Global configuration allows adding middleware (e.g., logging, error tracking, optimistic updates) to all requests.
Cons
1. Tight Coupling of Configuration
- All SWR hooks inherit the root config, which can make per-component customization slightly trickier.
- You’d need to override configs locally using useSWR(key, fetcher, options).
2. Global Cache Lifetime Management
- All data shares the same cache instance.
- Risk: stale or conflicting data if you fetch resources with the same key in different contexts but with different expectations.
3. Potential Overhead for Small Apps
- If your app only has a few API calls or you don’t need global caching, the root provider adds a layer without much benefit.
4. Hydration Challenges (Next.js)
- For SSR/SSG, you need to carefully handle SWR hydration (fallback prop) if you want server-fetched data to be available client-side.
- Root provider simplifies it, but misconfiguration can lead to extra fetches on the client.
5. Debugging Complexity
- When caching or revalidation issues occur, the global provider makes it harder to trace which component triggered a fetch, because all share the same cache.
Rule of Thumb
- Use a root SWRProvider if:
- Your app has multiple components fetching the same remote data.
- You want a unified caching and fetching policy.
- You plan to use global middleware or deduplication.
- Skip root provider if:
- Your data fetching needs are highly isolated or mostly component-local.
- You want fine-grained per-hook configuration with minimal global interference.