Introduction to SWR: A Stale-While-Revalidate Data Fetching Library for React

React best practices discourage direct data fetching inside Effects, and recommend using dedicated, full-featured data fetching solutions instead. This approach not only makes code easier to manage and understand, but also solves common pain points like:

  1. Race conditions
  2. Response caching
  3. Server-side data fetching support
  4. Preventing network waterfalls
  5. And more...

What is SWR

SWR is a React Hooks library built specifically for data fetching, named after the stale-while-revalidate caching strategy: it serves cached data to users immediately, while re-fetching fresh data in the background.

Traditional React Data Fetching Example

Before exploring SWR's features, let's look at a typical hand-written data fetching implementation in React:

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const UserProfile = () => {
  const [profileData, setProfileData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [requestError, setRequestError] = useState(null);

  useEffect(() => {
    const loadUserData = async () => {
      try {
        setIsLoading(true);
        const res = await axios.get('https://api.example.com/user');
        setProfileData(res.data);
      } catch (err) {
        setRequestError(err);
      } finally {
        setIsLoading(false);
      }
    };

    loadUserData();

    return () => {
      // Clean up ongoing requests here
    };
  }, []);

  if (isLoading) return <div>Loading user data...</div>;
  if (requestError) return <div>Error: {requestError.message}</div>;
  if (!profileData) return null;

  return (
    <div>
      <h1>User Profile</h1>
      <p>Full Name: {profileData.name}</p>
      <p>Contact Email: {profileData.email}</p>
    </div>
  );
};

export default UserProfile;

SWR Implementation Example

Using SWR, you can drastically simplify data fetching and state management code, while getting extra performance and automatic updates out of the box:

import React from 'react';
import useSWR from 'swr';
import axios from 'axios';

const fetcher = async (endpoint) => {
  const res = await axios.get(endpoint);
  return res.data;
};

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

  if (isLoading) return <div>Loading user data...</div>;
  if (requestError) return <div>Error: {requestError.message}</div>;
  if (!profileData) return <div>Loading user data...</div>;

  return (
    <div>
      <h1>User Profile</h1>
      <p>Full Name: {profileData.name}</p>
      <p>Contact Email: {profileData.email}</p>
    </div>
  );
};

export default UserProfile;

Any data request has three possible states: loading, ready, or error. SWR exposes data, error, and isLoading to let you render the correct UI for each state, with zero manual state management.

Key Advantages of SWR

1. Automatic Cache Management

SWR automatically caches fetched data, and returns cached data immediately for identical subsequent requests, greatly improving application responsiveness and overall performance.

2. Automatic Background Updates

SWR automatically revalidates data in the background. When data expires or the component re-renders, it triggers a new request and updates the component state automatically, keeping your UI data always fresh.

3. Automatic Retry on Failure

When a request fails due to network issues, SWR automatically retries the request out of the box, eliminating the need for developers to write custom retry logic manually.

4. Minimal Boilerplate

Compared to traditional hand-written data fetching code, SWR cuts down on redundant code significantly, making your codebase more readable and maintainable.

Reusable Data Hook Pattern

When building web applications, you often need to reuse the same data across multiple parts of your UI. SWR makes it extremely easy to create reusable data hooks:

function useUserProfile(userId) {
  const { data, error, isLoading } = useSWR(`/api/users/${userId}`, fetcher);
 
  return {
    profile: data,
    isLoading,
    hasError: error
  };
}

You can use this custom hook directly in any component that needs user data:

function UserAvatar({ userId }) {
  const { profile, isLoading, hasError } = useUserProfile(userId);
 
  if (isLoading) return <LoadingSpinner />;
  if (hasError) return <ErrorFallback />;
  return <img src={profile.avatarUrl} alt={`${profile.name}'s avatar`} />;
}

With this pattern, you no longer need to handle data fetching imperatively: start request, update loading states, pass results around. Instead, your code becomes declarative: you just specify what data your component needs.

Real-World Usage Example

Let's consider a common real-world scenario: a site's navigation bar and main content both depend on the current authenticated user's data:

  1. The navigation bar displays the user's avatar
  2. The main content shows a welcome message with the user's name

Traditionally, you would fetch the data once in the top-level page component with useEffect, then pass it down to child components via props (we'll skip error handling for this example):

// Top-level page component
function HomePage() {
  const [currentUser, setCurrentUser] = useState(null);
 
  useEffect(() => {
    fetch("/api/user")
      .then((res) => res.json())
      .then((userData) => setCurrentUser(userData));
  }, []);
 
  if (!currentUser) return <LoadingSpinner />;
 
  return (
    <div>
      <NavigationBar user={currentUser} />
      <MainContent user={currentUser} />
    </div>
  );
}
 
// Child components
function NavigationBar({ user }) {
  return (
    <div>
      ...
      <UserAvatar user={user} />
    </div>
  );
}
 
function MainContent({ user }) {
  return <h1>Welcome back, {user.name}</h1>;
}
 
function UserAvatar({ user }) {
  return <img src={user.avatarUrl} alt={user.name} />;
}

This approach requires all data requests to live in top-level components, and you have to drill props through every level of the component tree to get data to deep components. As you add more data dependencies, the code becomes increasingly hard to maintain.

While React Context can eliminate prop drilling, it still doesn't solve the problem of dynamic data: deep components may require different data that the top-level component doesn't know about ahead of time.

SWR solves this problem perfectly. Using a custom user hook, we can refactor the code like this:

function useCurrentUser() {
  const { data, error, isLoading } = useSWR(`/api/user`, fetcher);
 
  return {
    user: data,
    isLoading,
    isError: error
  };
}

// Top-level page component
function HomePage() {
  return (
    <div>
      <NavigationBar />
      <MainContent />
    </div>
  );
}
 
// Child components
function NavigationBar() {
  return (
    <div>
      ...
      <UserAvatar />
    </div>
  );
}
 
function MainContent() {
  const { user, isLoading } = useCurrentUser();
  if (isLoading) return <LoadingSpinner />;
  return <h1>Welcome back, {user.name}</h1>;
}
 
function UserAvatar() {
  const { user, isLoading } = useCurrentUser();
  if (isLoading) return <LoadingSpinner />;
  return <img src={user.avatarUrl} alt={user.name} />;
}

Now data is directly bound to the component that needs it, and all components are fully independent. None of the consuming components need to worry about how data is fetched or passed around—they just render based on the data they get.

This makes the code much simpler and easier to maintain. Even better, only one request is ever sent to the API, because all components use the same SWR cache key. The request is automatically deduplicated, cached, and shared across all components.

On top of that, SWR will automatically re-fetch data when the user re-focuses the browser tab or when the network reconnects. This means when a user wakes they computer from sleep or switches back to your tab after browsing other sites, your data will automatically refresh to the latest version.

Core SWR Features Explained

Automatic Revalidation

1. Revalidation on Focus

When you switch back to a tab containing your React app, SWR automatically revalidates and refreshes data to keep it up to date. This is extremely useful for keeping content synchronized after the tab has been in the background for a long time, or after the computer wakes from sleep.

To implement this behavior manually in plain React, you would use the Page Visibility API to detect when the page becomes visible again, then trigger a data refresh:

import React, { useEffect } from 'react';

const AutoRefreshComponent = () => {
  useEffect(() => {
    const handleVisibilityChange = () => {
      if (document.visibilityState === 'visible') {
        console.log('Page visible, refreshing data...');
        fetchFreshData();
      }
    };

    document.addEventListener('visibilitychange', handleVisibilityChange);

    return () => {
      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
  }, []);

  const fetchFreshData = async () => {
    try {
      const response = await fetch('https://api.example.com/data');
      const freshData = await response.json();
      console.log('Updated data:', freshData);
      // Update component state with new data
    } catch (err) {
      console.error('Failed to refresh data:', err);
    }
  };

  return (
    <div>
      <h1>Auto-refreshing Data Component</h1>
    </div>
  );
};

export default AutoRefreshComponent;

2. Periodic Revalidation

When data changes frequently across multiple clients or devices, you need to keep the UI updated over time. SWR supports automatic periodic revalidation, which is smart enough to only run refresh when the component is actually visible on the screen.

You can enable periodic revalidation with the refreshInterval option:

useSWR('/api/todos', fetcher, { refreshInterval: 1000 })

Additional options like refreshWhenHidden and refreshWhenOffline let you customize this behavior, both of which are disabled by default to avoid unnecessary network requests when the page is hidden or offline.

To implement this manually in plain React, the most common approach is to use a JavaScript interval timer to trigger periodic fetches:

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const PeriodicRefreshComponent = () => {
  const [freshData, setFreshData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [requestError, setRequestError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setIsLoading(true);
        const response = await axios.get('https://api.example.com/data');
        setFreshData(response.data);
      } catch (err) {
        setRequestError(err);
      } finally {
        setIsLoading(false);
      }
    };

    fetchData();

    const refreshInterval = setInterval(() => {
      fetchData();
    }, 60000); // Refresh once per minute

    return () => {
      clearInterval(refreshInterval);
    };
  }, []);

  if (isLoading) return <div>Loading...</div>;
  if (requestError) return <div>Error: {requestError.message}</div>;
  if (!freshData) return null;

  return (
    <div>
      <h1>Auto-refreshing Data</h1>
      <p>Current Value: {freshData}</p>
    </div>
  );
};

export default PeriodicRefreshComponent;

Alternative approaches include using WebSockets for real-time pushes from the server, or using pre-built hooks from libraries like react-use to simplify interval management. All of these require manual setup compared to SWR's built-in support.

3. Revalidation on Network Reconnect

SWR automatically re-fetches data when the user's network connection is restored after being disconnected. This is very useful for cases where the user unlocks their device before the network reconnects, ensuring data stays up to date.

To implement this manually, you would listen for the browser's online event to detect when the network comes back, then trigger a manual refresh:

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const ReconnectRefreshComponent = () => {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setIsLoading(true);
        const response = await axios.get('https://api.example.com/data');
        setData(response.data);
      } catch (err) {
        setError(err);
      } finally {
        setIsLoading(false);
      }
    };

    fetchData();

    const handleNetworkReconnect = () => {
      if (navigator.onLine) {
        fetchData();
      }
    };

    window.addEventListener('online', handleNetworkReconnect);

    return () => {
      window.removeEventListener('online', handleNetworkReconnect);
    };
  }, []);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!data) return null;

  return (
    <div>
      <h1>Auto-refresh on Reconnect</h1>
      <p>Data: {data}</p>
    </div>
  );
};

export default ReconnectRefreshComponent;

Conditional Data Fetching

1. On-Demand Fetching

You can conditionally enable or disable fetching by passing null as the SWR key, or a function that returns a falsy value. If the function throws an error or returns a falsy value, SWR will not start the request.

// Conditionally fetch based on a flag
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
 
// Return a falsy value from a function
const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)
 
// Automatically skips fetch until user.id is available
const { data } = useSWR(() => `/api/data?uid=${user.id}`, fetcher)

2. Dependent Requests

SWR also makes it easy to fetch data that depends on other data, helping you avoid network waterfalls while supporting sequential requests when needed:

function MyProjectsList () {
  const { data: currentUser } = useSWR('/api/user')
  const { data: userProjects } = useSWR(() => `/api/projects?uid=${currentUser.id}`)

  if (!userProjects) return 'Loading projects...'
  return `You have ${userProjects.length} projects`
}

When you pass a function as the key, SWR will use the return value as the cache key. If the function throws or returns a falsy value, SWR knows the required dependencies are not ready yet, and will wait to start the request.

Data Mutations

SWR provides two APIs for mutating remote data and updating the associated cache: mutate and useSWRMutation.

mutate: Updating Local and Remote Data

There are two ways to use the mutate API: a global mutate that can update any cached key, and a bound mutate that only updates the data for the current SWR hook.

  1. Global Mutation
import { useSWRConfig } from "swr"
function App() {
  const { mutate } = useSWRConfig()
  mutate(cacheKey, newData, options)
}

// Or import the global mutate directly
import { mutate } from "swr"
function App() {
  mutate(cacheKey, newData, options)
}

If you call the global mutate with only a key, SWR will mark that key as stale and trigger a revalidation, but will not update the cache unless a matching SWR hook is mounted.

  1. Bound Mutation Bound mutate is bound to the cache key of the current useSWR hook, making it more convenient to update data for the current request:
import useSWR from 'swr'
 
function UserProfile () {
  const { data, mutate } = useSWR('/api/user', fetcher)
 
  return (
    <div>
      <h1>My name is {data.name}.</h1>
      <button onClick={async () => {
        const formattedName = data.name.toUpperCase()
        await updateUsernameAPI(formattedName)
        // Update local cache immediately, then trigger revalidation
        mutate({ ...data, name: formattedName })
      }}>Uppercase my name!</button>
    </div>
  )
}
  1. Triggering Revalidation If you call mutate(key) without passing new data, or call the bound mutate() with no arguments, SWR will mark the data as stale and trigger a new revalidation request automatically:
import useSWR, { useSWRConfig } from 'swr'
 
function App () {
  const { mutate } = useSWRConfig()
 
  return (
    <div>
      <UserProfile />
      <button onClick={() => {
        // Clear authentication token
        document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
        // Trigger revalidation for the user data to get the logged out state
        mutate('/api/user')
      }}>
        Logout
      </button>
    </div>
  )
}

useSWRMutation: Triggering Remote Mutations

SWR also provides useSWRMutation, a dedicated hook for triggering remote mutations manually. Unlike useSWR, it will not automatically send any requests, and does not share state with other useSWRMutation hooks.

import useSWRMutation from 'swr/mutation'
 
// The mutation fetcher receives the URL and an extra `arg` parameter for input data
async function updateUserName(url, { arg }) {
  await fetch(url, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${arg.token}`
    },
    body: JSON.stringify({name: arg.newName})
  })
}
 
function Profile() {
  const { trigger } = useSWRMutation('/api/user', updateUserName, options)
 
  return <button onClick={() => {
    trigger({ token: 'my_auth_token', newName: 'John Doe' })
  }}>Update User Name</button>
}

Basic usage example:

import useSWRMutation from 'swr/mutation'
 
async function createUser(url, { arg }) {
  return fetch(url, {
    method: 'POST',
    body: JSON.stringify(arg)
  }).then(res => res.json())
}
 
function CreateUserForm() {
  const { trigger, isMutating } = useSWRMutation('/api/user', createUser)
 
  return (
    <button
      disabled={isMutating}
      onClick={async () => {
        try {
          const result = await trigger({ username: 'johndoe' })
          // Handle success
        } catch (e) {
          // Handle error
        }
      }}
    >
      Create New User
    </button>
  )
}

You can access the result of the mutation from the hook's return value if you need to use it in rendering:

const { trigger, data, error } = useSWRMutation('/api/user', createUser)

useSWRMutation shares the same cache as useSWR, so it avoids race conditions automatically. It also supports all mutation features like optimistic updates and rollback on error, which you can configure as options:

const { trigger } = useSWRMutation('/api/user', updateUserName, {
  optimisticData: current => ({ ...current, name: newName })
})
 
// Or pass options to the trigger call
trigger(newName, {
  optimisticData: current => ({ ...current, name: newName })
})

useSWRMutation is ideal for lazy mutations that only need to run when triggered by a user action, since it will never send a request until you explicitly call trigger.

To learn more about SWR's internal state mechanism, caching system, performance optimizations, developer tools, React Native support and other advanced topics, refer to the official SWR advanced documentation.

Tags: React SWR Data Fetching Frontend Development javascript

Posted on Wed, 30 Sep 2026 16:48:46 +0000 by MitchEvans