Using SWR for Data Fetching and Caching

Introduction

Data fetching is one of the most common tasks in any React application. Whether you’re building dashboards, feeds, or analytics tools, your components often depend on external data sources like APIs. However, fetching and managing data efficiently — while maintaining performance and consistency — can be a challenge.

React developers have several tools to handle data fetching, from fetch() and axios to custom hooks and libraries like React Query or SWR. Among them, SWR (which stands for stale-while-revalidate) has gained popularity due to its simplicity, performance, and automatic caching behavior.

Developed by Vercel, the team behind Next.js, SWR provides a reactive data fetching mechanism that ensures your UI is always fast, up-to-date, and reliable. In this post, we’ll explore how SWR works, why it’s beneficial, and how to use it effectively in React applications for optimized data fetching and caching.


What is SWR?

SWR stands for Stale-While-Revalidate, which is a caching strategy originally from the HTTP RFC 5861 standard.

The concept works like this:

  1. When data is requested, SWR first returns the cached data (stale) immediately.
  2. Then, it fetches fresh data (revalidate) in the background.
  3. Once the new data is retrieved, SWR updates the cache and re-renders the component automatically.

This creates a smooth user experience:

  • The UI never stays empty while waiting for data.
  • Data stays fresh with automatic revalidation.
  • Network requests are minimized because of intelligent caching.

Key Benefits of SWR

Using SWR in React applications brings several advantages:

  1. Automatic Caching
    SWR automatically caches fetched data and reuses it between renders.
  2. Stale-While-Revalidate
    Returns cached data instantly and revalidates it in the background.
  3. Auto Revalidation
    Data is revalidated automatically when the user refocuses the window or reconnects the network.
  4. Performance Optimization
    Reduces network calls and speeds up page load times.
  5. Simple API
    SWR uses a minimal, hook-based API that integrates perfectly with React’s component model.
  6. Error and Loading State Management
    Provides built-in states for loading and error handling without extra configuration.
  7. TypeScript and SSR Support
    Works seamlessly with TypeScript and Next.js, making it ideal for modern web apps.

Installing SWR

To start using SWR, you first need to install it in your React project.

npm install swr

or

yarn add swr

Once installed, you can start importing its hooks in your React components.


Basic Usage of SWR

The primary hook provided by SWR is useSWR(). It takes two arguments:

  1. A key – usually the API endpoint or unique identifier for the resource.
  2. A fetcher function – a function responsible for fetching the data.

Example: Basic SWR Hook

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());

function UserProfile() {
  const { data, error, isLoading } = useSWR('https://api.example.com/user/1', fetcher);

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error loading data.</p>;

  return (
&lt;div&gt;
  &lt;h2&gt;{data.name}&lt;/h2&gt;
  &lt;p&gt;Email: {data.email}&lt;/p&gt;
&lt;/div&gt;
); } export default UserProfile;

Here’s what happens:

  • SWR checks if the data for 'https://api.example.com/user/1' is in the cache.
  • If cached data exists, it’s returned immediately (stale data).
  • SWR then fetches fresh data in the background.
  • Once the data arrives, the component automatically re-renders.

Understanding the Fetcher Function

The fetcher function is the core of SWR’s functionality. It defines how the data is fetched.
You can use the native fetch() API, axios, or any other HTTP client.

Example: Using Axios as Fetcher

import useSWR from 'swr';
import axios from 'axios';

const fetcher = (url) => axios.get(url).then((res) => res.data);

function PostsList() {
  const { data, error } = useSWR('https://api.example.com/posts', fetcher);

  if (error) return <p>Failed to load posts.</p>;
  if (!data) return <p>Loading...</p>;

  return (
&lt;ul&gt;
  {data.map((post) =&gt; (
    &lt;li key={post.id}&gt;{post.title}&lt;/li&gt;
  ))}
&lt;/ul&gt;
); } export default PostsList;

SWR does not depend on any specific HTTP library, making it flexible and easy to integrate into any existing setup.


Handling Loading and Error States

The useSWR hook provides three key states:

  • data: Contains the fetched or cached data.
  • error: Captures any errors from the fetcher.
  • isLoading: Indicates whether the request is still in progress.

These states make UI feedback simple and declarative.


Revalidation Behavior

SWR automatically revalidates data in several scenarios:

  1. When the component mounts
    SWR fetches fresh data if it’s not in cache.
  2. When the window regains focus
    If the user switches tabs and comes back, SWR revalidates data automatically.
  3. When the network reconnects
    If the device was offline, SWR refetches data once the connection is restored.
  4. Manual revalidation
    You can trigger a revalidation programmatically using the mutate() function.

Manual Revalidation with mutate()

The mutate() function allows you to update the cache manually or trigger a revalidation.

Example: Manual Update

import useSWR, { mutate } from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());

function RefreshButton() {
  const { data } = useSWR('/api/user', fetcher);

  return (
&lt;div&gt;
  &lt;h3&gt;{data?.name}&lt;/h3&gt;
  &lt;button onClick={() =&gt; mutate('/api/user')}&gt;Refresh Data&lt;/button&gt;
&lt;/div&gt;
); } export default RefreshButton;

When the button is clicked, mutate('/api/user') triggers revalidation and fetches the latest data from the server.


Local Mutations (Optimistic Updates)

SWR supports optimistic UI updates, where the UI updates instantly before the server confirms the change.

Example: Optimistic UI Update

import useSWR, { mutate } from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());

function LikePost({ postId }) {
  const { data } = useSWR(/api/posts/${postId}, fetcher);

  const handleLike = async () => {
mutate(/api/posts/${postId}, { ...data, likes: data.likes + 1 }, false);
await fetch(/api/posts/${postId}/like, { method: 'POST' });
mutate(/api/posts/${postId});
}; if (!data) return <p>Loading...</p>; return (
&lt;div&gt;
  &lt;p&gt;{data.title}&lt;/p&gt;
  &lt;button onClick={handleLike}&gt;Like ({data.likes})&lt;/button&gt;
&lt;/div&gt;
); } export default LikePost;

Here’s what happens:

  1. The UI immediately increments the like count.
  2. The API request runs in the background.
  3. Once the API confirms, SWR revalidates the data.

This gives users instant feedback without waiting for a server response.


Global Configuration with SWRConfig

You can set up global configurations for all SWR hooks using the SWRConfig provider.

Example: Global Fetcher

import { SWRConfig } from 'swr';
import axios from 'axios';
import App from './App';

const fetcher = (url) => axios.get(url).then((res) => res.data);

function Root() {
  return (
&lt;SWRConfig value={{ fetcher, dedupingInterval: 10000 }}&gt;
  &lt;App /&gt;
&lt;/SWRConfig&gt;
); } export default Root;

Now, all useSWR() calls in your app will use this fetcher and share the same cache.


Data Deduplication

When multiple components use the same SWR key, only one network request is made.
All components share the same cached data and update simultaneously when revalidated.

This deduplication ensures efficiency and prevents redundant requests.


Pagination and Infinite Loading

SWR provides a special hook called useSWRInfinite() for handling pagination or infinite scroll patterns.

Example: Infinite Loading

import useSWRInfinite from 'swr/infinite';

const fetcher = (url) => fetch(url).then((res) => res.json());

function PaginatedPosts() {
  const getKey = (pageIndex, previousPageData) => {
if (previousPageData &amp;&amp; !previousPageData.length) return null;
return /api/posts?page=${pageIndex + 1};
}; const { data, size, setSize } = useSWRInfinite(getKey, fetcher); const posts = data ? [].concat(...data) : []; return (
&lt;div&gt;
  {posts.map((post) =&gt; (
    &lt;p key={post.id}&gt;{post.title}&lt;/p&gt;
  ))}
  &lt;button onClick={() =&gt; setSize(size + 1)}&gt;Load More&lt;/button&gt;
&lt;/div&gt;
); } export default PaginatedPosts;

This pattern allows for efficient infinite scrolling where each page is cached and automatically revalidated when needed.


Dependent Data Fetching

SWR supports dependent requests, where one API call depends on another’s data.

Example: Dependent Calls

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());

function UserPosts() {
  const { data: user } = useSWR('/api/user', fetcher);
  const { data: posts } = useSWR(
user ? /api/posts?userId=${user.id} : null,
fetcher
); if (!user || !posts) return <p>Loading...</p>; return (
&lt;div&gt;
  &lt;h2&gt;{user.name}'s Posts&lt;/h2&gt;
  {posts.map((post) =&gt; (
    &lt;p key={post.id}&gt;{post.title}&lt;/p&gt;
  ))}
&lt;/div&gt;
); } export default UserPosts;

SWR won’t start fetching posts until the user data is available, ensuring efficient and conditional data loading.


SWR with Next.js

SWR integrates perfectly with Next.js, especially for client-side data fetching.

You can use it with static generation (SSG) and server-side rendering (SSR) to combine performance and freshness.

Example: SWR with Next.js Page

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());

export default function Home() {
  const { data, error } = useSWR('/api/posts', fetcher);

  if (error) return <p>Error loading posts.</p>;
  if (!data) return <p>Loading...</p>;

  return (
&lt;div&gt;
  &lt;h1&gt;Latest Posts&lt;/h1&gt;
  {data.map((post) =&gt; (
    &lt;p key={post.id}&gt;{post.title}&lt;/p&gt;
  ))}
&lt;/div&gt;
); }

SWR handles the client-side fetching while Next.js manages server-side rendering for improved performance.


SWR vs React Query

FeatureSWRReact Query
DeveloperVercelTanner Linsley
FocusSimplicity and cachingComprehensive data management
MutationSimple via mutate()Advanced mutation APIs
Query KeysSimple stringsHierarchical query keys
Bundle SizeSmallerLarger
Suitable ForLightweight appsComplex state management

Both are excellent libraries, but SWR is ideal when you want lightweight, declarative, and automatic data fetching with minimal configuration.


Performance Optimizations with SWR

  1. Adjust revalidateOnFocus and revalidateOnReconnect
    Disable these for static or low-change data.
  2. Use dedupingInterval
    Prevent repeated fetches within a time interval.
  3. Leverage fallbackData
    Provide default cached data for faster initial rendering.
  4. Combine with Suspense
    Integrate SWR’s loading behavior with React’s Suspense for elegant fallbacks.

Example: Advanced SWR Configuration

import { SWRConfig } from 'swr';
import App from './App';

function Root() {
  return (
&lt;SWRConfig
  value={{
    revalidateOnFocus: true,
    revalidateOnReconnect: true,
    dedupingInterval: 5000,
    refreshInterval: 0,
    fetcher: (url) =&gt; fetch(url).then((res) =&gt; res.json()),
  }}
&gt;
  &lt;App /&gt;
&lt;/SWRConfig&gt;
); } export default Root;

These settings ensure SWR behaves efficiently across your entire app.


Common Mistakes to Avoid

  1. Using SWR without a unique key – Each resource should have a distinct key for proper caching.
  2. Not using null for conditional fetching – Prevents premature or unnecessary API calls.
  3. Ignoring cache invalidation – Always revalidate when data updates.
  4. Over-fetching – Adjust revalidation intervals to balance freshness and performance.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *