Unlock the Power of Stale-While-Revalidate for Seamless Performance and Effortless API Management in React
React SWR is one of those libraries that, once you start using it, you wonder how you ever lived without it. If you're dealing with data fetching in React and want a smooth experience with caching, revalidation, and performance optimizations, SWR is a game-changer.
In this article, let's go deep into how SWR works, what makes it different, how it optimizes performance, and a quick comparison with TanStack Query. We’ll also look at how to integrate SWR in a Next.js application with a custom fetcher.
SWR stands for stale-while-revalidate, a strategy that serves stale data from the cache while fetching fresh data in the background. This ensures that your app remains fast and responsive while keeping the data up-to-date.
The best part? It's built by the team at Vercel, the folks behind Next.js, so you already know it's optimized for modern React applications.
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then(res => res.json())
export default function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Failed to load</div>
return <div>Hello, {data.name}!</div>
}
With just a few lines of code, SWR handles fetching, caching, revalidation, and error handling. But how does it actually work under the hood?
When useSWR
runs for the first time, it does two things:
It checks if there's cached data for the given key (/api/user
).
If cached data exists, it returns it immediately (stale data).
Meanwhile, it triggers a network request in the background to fetch fresh data.
Once the fresh data arrives, it updates the cache and re-renders the component.
SWR uses an in-memory cache, which means it persists data across component unmounts. The cache operates based on the key, so different API endpoints have independent caches.
SWR doesn't just fetch data once and call it a day. It automatically revalidates the data in several scenarios:
Focus Revalidation: When the user switches back to your tab, SWR refetches the data.
Interval Revalidation: You can set an interval to refresh the data periodically.
Mutation Revalidation: If some action updates the data, SWR ensures the UI stays in sync.
Example of interval revalidation:
const { data } = useSWR('/api/user', fetcher, { refreshInterval: 5000 })
This keeps your UI always up-to-date without users having to refresh manually.
If multiple components request the same data at the same time, SWR ensures that only one network request is sent. The results are then shared across all components.
Under the hood, SWR maintains a registry of ongoing fetch requests. If another fetch request is triggered for the same key before the first completes, SWR attaches the new request to the existing one, preventing unnecessary duplicate requests.
SWR allows you to manually trigger updates for a specific key, which is useful when performing an action like submitting a form.
Example:
import { mutate } from 'swr'
async function updateUser(newData) {
mutate('/api/user', { ...newData }, false) // Update the UI instantly
await fetch('/api/user', { method: 'PUT', body: JSON.stringify(newData) })
mutate('/api/user') // Refetch data after the update
}
This enables optimistic updates, where the UI updates immediately before the network request completes.
SWR uses an in-memory cache for storing fetched data, but you can also persist the cache between reloads using libraries like swr-persist
. This makes your app feel faster, even after refreshes.
Example of using localStorage to persist SWR cache:
import { cache } from 'swr'
window.addEventListener('beforeunload', () => {
localStorage.setItem('swr-cache', JSON.stringify(cache.serialize()))
})
window.addEventListener('load', () => {
const cachedData = localStorage.getItem('swr-cache')
if (cachedData) {
cache.deserialize(JSON.parse(cachedData))
}
})
Instead of blocking UI updates, SWR fetches data in the background and updates the UI only when necessary, making it much more efficient than traditional polling.
SWR supports conditional fetching, where it waits for dependencies before making a request.
Example:
const { data: user } = useSWR('/api/user', fetcher)
const { data: posts } = useSWR(user ? `/api/posts?user=${user.id}` : null, fetcher)
Here, the posts
request waits until user
is fetched, avoiding unnecessary API calls.
SWR supports React Suspense, which simplifies loading states by allowing Suspense
to handle them.
const { data } = useSWR('/api/user', fetcher, { suspense: true })
export default function Profile() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile />
</Suspense>
)
}
Suspense makes loading states declarative and cleaner.
A fetcher function is required for SWR to fetch data from an API. Instead of writing fetch(url).then(res => res.json())
everywhere, we can create a centralized fetcher.
Here’s a robust fetcher that handles GET, POST, PUT, DELETE requests:
const fetcher = async (url: string, options?: RequestInit) => {
const res = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...(options?.headers || {})
}
})
if (!res.ok) {
throw new Error(`HTTP error! Status: ${res.status}`)
}
return res.json()
}
This fetcher:
Automatically sets Content-Type
to application/json
.
Merges custom headers.
Throws an error if the request fails.
Now, we can use this fetcher in a Next.js page component.
import useSWR from 'swr'
export default function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Failed to load</div>
return <div>Hello, {data.name}!</div>
}
For mutations (like submitting a form), we can use mutate
to update the local cache immediately.
async function updateUser(newData) {
mutate('/api/user', { ...newData }, false) // Optimistically update UI
await fetcher('/api/user', { method: 'PUT', body: JSON.stringify(newData) })
mutate('/api/user') // Refetch fresh data
}
The default fetch API in JavaScript is simple but lacks many features needed for a modern application, such as:
Automatic handling of different content types (JSON, text, blobs, etc.)
Authentication headers management
Better error handling with meaningful responses
Flexibility to handle external and internal API routes differently
By creating a custom fetcher, we encapsulate all of these features in one reusable function that makes our API calls consistent and reliable.
SWR is great, but how does it compare with TanStack Query?
Caching Strategy: SWR always serves stale data first, then revalidates. TanStack Query allows fine-grained control over stale time and cache expiration.
Complex State Management: SWR is stateless; each request is independent. TanStack Query manages global query states, making it better for apps with complex data dependencies.
Pagination & Infinite Queries: SWR requires manual implementation for pagination. TanStack Query provides built-in support.
Flexibility: SWR is lightweight and designed for simpler real-time fetching. TanStack Query is powerful but requires more configuration.
Developer Experience: SWR requires fewer dependencies and minimal setup. TanStack Query provides more features but comes with additional complexity.
When simplicity and ease of use matter.
When you need real-time data with automatic background updates.
When you're building a Next.js app and want seamless data-fetching.
When your API responses are not deeply interdependent.
When you need advanced query management (pagination, infinite scroll, dependent queries).
When your app heavily relies on API state synchronization.
When your API calls are expensive, and caching needs to be fine-tuned.
React SWR is an elegant, lightweight solution for handling data fetching in React. If you need fast, automatic caching and revalidation, SWR is the perfect tool.
For developers, SWR provides:
A seamless experience with minimal setup.
Performance optimizations that reduce redundant API calls.
A clean API for declarative, real-time data fetching.
A great fit for Next.js and modern React applications.
If your app is data-heavy, requires advanced state management, or deals with paginated results, TanStack Query might be a better fit. But for most cases, SWR is a game-changer for building fast and efficient apps.
- Jagadhiswaran Devaraj
📢 Stay Connected & Dive Deep into Tech!
🚀 Follow me for hardcore technical insights on JavaScript, Full-Stack Development, AI, and Scaling Systems:
🐦 X (Twitter): jags
✍️ Read more on Medium: https://medium.com/@jwaran78
💼 Connect with me on LinkedIn: https://www.linkedin.com/in/jagadhiswaran-devaraj/
Let’s geek out over code, architecture, and all things in tech! 💡🔥
Join Jagadhiswaran on Peerlist!
Join amazing folks like Jagadhiswaran and thousands of other people in tech.
Create ProfileJoin with Jagadhiswaran’s personal invite link.
0
3
0