Switch Language
Toggle Theme

Complete SWR Guide: Master Cache Strategies and Optimistic Updates in Practice

Friday afternoon, 3 PM. I’m staring at that familiar loading spinner for the Nth time, refreshing the user list page. Tab switch? Loading. Come back from another page? Loading. Even just taking a phone call that causes the browser to lose focus—come back and it’s loading again. The most frustrating part? That data was loaded just a second ago.

I bet you’ve been there too—writing React projects where every data fetch requires useState, useEffect, plus loading and error handling logic. 20 lines minimum. Even more annoying: when multiple components need the same data, you either lift state to a parent component (more code) or have each component make its own request (wasting network resources).

Honestly, I used to think I was just doing it wrong. Then I discovered SWR and had that “holy crap, you can do it like this?” moment—kind of like the first time using Git instead of manually copying folders. Three lines of code solved 80% of the problem. No exaggeration.

Today let’s talk about SWR—Vercel’s React data fetching library. The core concept boils down to one term: stale-while-revalidate. Sounds technical? The principle is super simple: show you the cached “old photo” first (fast), secretly fetch a new one in the background (accurate), then quietly swap it out when done (seamless).

Why Do We Need SWR? Three Pain Points of Traditional Data Fetching

Let’s first look at what traditional React data fetching looks like. Say you want to display a user list:

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        setUsers(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <ul>{users.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

24 lines of code. Just to display a list. You might think it’s okay? Consider these scenarios:

Pain Point 1: Repetitive Boilerplate Code

Every component that needs data has to be written this way. User list needs it, article list needs it, comment list needs it. Three states, a bunch of if statements, error handling…copy-pasting feels embarrassing. Once I counted in a medium-sized project—this boilerplate code accounted for 20% of the codebase.

Pain Point 2: Cache? What Cache?

The real pain is no caching. Users navigate from homepage to detail page, then back to homepage—another long loading screen. Data was fetched 30 seconds ago but needs to be requested again. Users complain “why is your website so slow” when really the API isn’t slow—we just have zero caching.

Someone might say, “just write your own global state management.” Yeah, you could. Then you maintain Redux/Zustand stores, actions, reducers…what was a 3-minute feature becomes 30 minutes of architectural design.

Pain Point 3: Multi-Component Data Sync is a Nightmare

Even worse is when multiple components use the same data. Top navigation shows unread message count, sidebar shows it, message list page shows it too. What do you do? Lift state to the top level? Then every update passes through a dozen prop layers. Use Context? Then every message update re-renders the entire tree.

I remember once modifying a notification feature—the logic was simple, but I spent two days handling state sync issues. When committing code, seeing all that setState and useEffect everywhere, I couldn’t even look at it myself.

That’s why SWR caught on. It’s not just another wheel—it actually solves real pain points. Rewrite the above code with SWR?

import useSWR from 'swr';

function UserList() {
  const { data, error, isLoading } = useSWR('/api/users', fetcher);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error!</div>;

  return <ul>{data.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

9 lines of code. Done. Built-in caching, auto-revalidate, cross-component sharing. Oh, and fetcher is just your fetch function, usually defined globally once:

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

The difference is obvious.

SWR’s Core Concept: The Stale-While-Revalidate Strategy

The name SWR comes from a cache invalidation strategy defined in HTTP RFC 5861. Don’t let “RFC” scare you—the principle is super easy to understand.

Using a photo analogy: You want to see a recent photo of a friend. Traditional approach—call them to take a new photo and send it, you wait. The SWR approach—flip through an old photo from last time you met (maybe from last week), while messaging them to take a new one and send it over, then replace the old one when it arrives.

What’s the key difference? You don’t have to wait idly. You can see content immediately (though possibly a bit old) while ensuring you eventually see the latest. That’s “stale-while-revalidate”—display the stale data while revalidating.

SWR’s Complete Workflow:

  1. Step One: Return Cache Immediately (stale)

    • When component mounts, SWR checks local cache first
    • Have cache? Return immediately, page opens instantly
    • No cache? Return undefined, show loading
  2. Step Two: Background Request (revalidate)

    • Regardless of cache presence, initiate API request
    • User doesn’t notice because they’re already viewing content
  3. Step Three: Update Data

    • After API returns, quietly update cache and UI
    • If data changed, React auto re-renders
    • If unchanged, do nothing

Let’s look at a real example. I’m building a stock price display page:

function StockPrice({ symbol }) {
  const { data, error } = useSWR(`/api/stock/${symbol}`, fetcher);

  return (
    <div>
      <h2>{symbol}</h2>
      <p>Price: {data ? `$${data.price}` : 'Loading...'}</p>
      <span>Updated: {data?.updatedAt}</span>
    </div>
  );
}

First time users open the page, data is undefined, shows “Loading…”. One second later API returns, displays price.

User switches to another tab to check email, comes back 10 minutes later—instantly sees the price (from cache), no loading flash. But simultaneously, SWR already initiated a new request in the background. If stock price changed, page auto-updates; if not, stays the same.

That experience? Buttery smooth.

Auto-Revalidation’s Three Triggers:

SWR by default automatically refetches data in these situations:

  1. Component Remount (revalidateOnMount): On page refresh or routing back
  2. Window Refocus (revalidateOnFocus): When user switches back to browser tab
  3. Network Reconnect (revalidateOnReconnect): After reconnecting from disconnect

What does this mean? You don’t manage anything—SWR automatically keeps data fresh. User checks email for half an hour, switches back to your site—SWR auto-refreshes data. User loses connection in subway, regains 4G at station—SWR auto-reloads.

Honestly, when I first learned these were default behaviors, I was stunned. Previously these scenarios either weren’t handled (users see stale data) or required manually listening to visibilitychange and online events, writing tons of code. Now? Free included.

The Key Concept of Keys:

You may have noticed useSWR’s first parameter is a string like '/api/users'. This is called a “key” and it’s very important.

// Two components using the same key
function Header() {
  const { data } = useSWR('/api/user', fetcher);
  return <div>Welcome, {data?.name}</div>;
}

function Profile() {
  const { data } = useSWR('/api/user', fetcher);
  return <div>Profile: {data?.email}</div>;
}

As long as keys match, data is shared. SWR only requests the API once, both components get the same data and auto-sync updates.

That’s why it solves the “multi-component data sync” problem. No state management needed, no Context required—just one key.

Deep Dive into Cache Strategy: Making Data Fetching Smarter

Earlier we mentioned SWR auto-caches and auto-revalidates. But in real projects, different data has different “freshness requirements.” User avatars might not change for a week, while stock prices need second-level updates. That’s when you need to adjust cache strategies.

Default Behavior is Smart, But Not Universal

SWR’s default config is actually quite aggressive:

  • Revalidate on every component mount
  • Revalidate on every window focus
  • Revalidate on every network reconnect

For real-time data (chat messages, online status), this is perfect. But for relatively stable data (article lists, user profiles), it’s a bit wasteful. Users switching between two tabs—send a request every time? Unnecessary.

Core Configuration Options

SWR provides tons of config options, but here are the ones you’ll actually use:

const { data } = useSWR('/api/articles', fetcher, {
  revalidateOnFocus: false,      // Don't refetch when window focused
  revalidateOnReconnect: false,  // Don't refetch when network reconnects
  refreshInterval: 0,            // Polling interval (milliseconds), 0 means no polling
  dedupingInterval: 2000,        // Dedupe same requests within 2 seconds
});

These config option names are straightforward—basically self-explanatory.

Scenario 1: Real-time Data (stocks, online users)

const { data } = useSWR('/api/stock/AAPL', fetcher, {
  refreshInterval: 1000,  // Poll every second
  revalidateOnFocus: true // Update immediately when switching back
});

This type of data has high timeliness requirements, ideally always latest. Polling is the simplest solution (of course, WebSocket is better, but that’s another topic).

Scenario 2: Relatively Stable Data (user info, article lists)

const { data } = useSWR('/api/profile', fetcher, {
  revalidateOnFocus: false,     // Don't refresh on every focus
  refreshInterval: 0,           // No polling
  dedupingInterval: 60000,      // Don't repeat requests within 1 minute
});

User profiles generally don’t change frequently. Tab switching doesn’t need refresh, saves requests. But if users actively modify their profile, you can still manually trigger updates (mutate, covered later).

Scenario 3: Nearly Static Content (docs, help pages)

const { data } = useSWR('/api/docs', fetcher, {
  revalidateOnFocus: false,
  revalidateOnReconnect: false,
  revalidateOnMount: false,     // Don't even validate on mount
  revalidateIfStale: false,     // Don't validate even if data is stale
});

This data might not change for a month. After initial load, it’s basically permanent cache unless users manually refresh the page.

Global vs Local Config

If your app’s strategy is fairly uniform, use SWRConfig to set global defaults:

import { SWRConfig } from 'swr';

function App() {
  return (
    <SWRConfig value={{
      refreshInterval: 3000,
      fetcher: (url) => fetch(url).then(r => r.json()),
      revalidateOnFocus: false,
    }}>
      <Dashboard />
    </SWRConfig>
  );
}

All child component useSWR calls inherit these configs. Of course, individual useSWR can still override:

// This component's revalidateOnFocus will be true, overriding global false
const { data } = useSWR('/api/realtime', fetcher, {
  revalidateOnFocus: true
});

Request Deduping: Save Your API Quota

There’s a very useful feature—automatic request deduping. Say you have three components mounting simultaneously, all using useSWR('/api/user') to fetch user data. SWR won’t send three requests—just one, with all three components sharing the result.

This “deduping window” defaults to 2 seconds (dedupingInterval: 2000). Meaning same requests within 2 seconds get merged. If your components might quickly remount (like rapidly switching tabs), you can increase this value.

Honestly, this feature saved me once. Had a list page where rapid scrolling triggered multiple data loads (my logic had issues), causing the same request to fire a dozen times within seconds. After adding this dedup config, it immediately normalized. Though treating the symptom not the cause, at least didn’t exhaust the API quota (laugh).

Conditional Fetching: Dependent Requests

Sometimes you need to fetch data A first, then fetch B based on A’s result. SWR has a little trick—pass null as key to pause requests:

// First fetch user info
const { data: user } = useSWR('/api/user', fetcher);

// Wait for user to load, then fetch user's project list
const { data: projects } = useSWR(
  user ? `/api/projects?userId=${user.id}` : null,
  fetcher
);

When user is undefined, key is null, SWR won’t send request. Once user loads, key becomes a valid value, SWR then starts requesting projects. Serial dependency, just that simple.

Optimistic Updates: The Secret Weapon for Better UX

Caching solved the “fast” problem, but there’s another scenario we haven’t covered—user actions. When users click “like”, do you make them wait for API response before showing the heart? Or immediately show the heart while sending the request in the background?

That’s the significance of Optimistic Updates—assume the operation will succeed, immediately update UI, roll back if it fails. Sounds risky? Actually most operations succeed (unless network issues or server crashes), and the UX improvement is visible.

Comparing Traditional vs Optimistic Updates

Traditional approach:

  1. User clicks “like”
  2. Button becomes loading state (or disabled)
  3. Wait 500ms-2s (depends on connection)
  4. API returns success, heart lights up
  5. User: “This website is kinda slow”

Optimistic update:

  1. User clicks “like”
  2. Heart lights up immediately (local update)
  3. Send API request in background
  4. (Usually) succeeds, nothing to do
  5. (Occasionally) fails, heart reverts to gray, show “operation failed”

The latter has 0ms delay. User feeling? Lightning fast.

The mutate Function: Manual Cache Control

SWR provides a mutate function to manually update cache. Simplest usage:

import { mutate } from 'swr';

// Manually trigger revalidation of /api/user
mutate('/api/user');

But optimistic updates need more control. Let’s look at a complete Todo app example:

import useSWR, { mutate } from 'swr';

function TodoList() {
  const { data: todos } = useSWR('/api/todos', fetcher);

  const addTodo = async (text) => {
    const newTodo = { id: Date.now(), text, completed: false };

    // Core: Optimistic update config
    mutate(
      '/api/todos',
      async (currentTodos) => {
        // 1. Immediately show new Todo in UI (optimistic)
        const optimisticData = [...currentTodos, newTodo];

        // 2. Send real request in background
        const savedTodo = await fetch('/api/todos', {
          method: 'POST',
          body: JSON.stringify(newTodo)
        }).then(r => r.json());

        // 3. Replace temp data with real data
        return [...currentTodos, savedTodo];
      },
      {
        optimisticData: [...todos, newTodo],  // Show immediately
        rollbackOnError: true,                // Rollback on failure
        revalidate: false,                    // No extra validation needed
      }
    );
  };

  return (
    <div>
      {todos?.map(todo => <div key={todo.id}>{todo.text}</div>)}
      <button onClick={() => addTodo('New task')}>Add</button>
    </div>
  );
}

This code is a bit long, but logic is clear:

  1. User clicks “Add”
  2. Immediately show new Todo in list (optimisticData)
  3. Send POST request in background
  4. If succeeds, replace with real server data (may have id, timestamp fields)
  5. If fails, rollback to previous state (rollbackOnError: true)

Four Core Options

mutate’s options object has four key configs:

1. optimisticData: Data to update immediately

optimisticData: [...todos, newTodo]  // Or pass a function

Can pass fixed value or function. Function form is more flexible:

optimisticData: (currentTodos) => [...currentTodos, newTodo]

2. populateCache: Whether to update cache with return value

Defaults to true. Most cases you want to update cache with API response (since server might add id, createdAt fields). But if API doesn’t return complete data, set to false.

3. revalidate: Whether to revalidate

Usually set to false. Because you manually updated data, don’t need SWR to send another request. Unless you don’t trust your update logic and want SWR to double-check.

4. rollbackOnError: Whether to rollback on failure

Super important. If true and mutate function throws error, SWR auto-rolls back to pre-update data. User clicked “like”, heart lit up, but API failed? SWR automatically reverts heart to gray.

Can also pass a function for finer control:

rollbackOnError: (error) => {
  // Don't rollback on network timeout, rollback on other errors
  return error.name !== 'AbortError';
}

In Practice: Delete Todo

Delete operations are also classic optimistic update scenarios:

const deleteTodo = async (id) => {
  mutate(
    '/api/todos',
    async (currentTodos) => {
      // Immediately remove from list
      const optimistic = currentTodos.filter(t => t.id !== id);

      // Send delete request in background
      await fetch(`/api/todos/${id}`, { method: 'DELETE' });

      // Return updated data
      return optimistic;
    },
    {
      optimisticData: todos.filter(t => t.id !== id),
      rollbackOnError: true,
    }
  );
};

User clicks delete, Todo immediately disappears. If server says “this Todo doesn’t exist” or request fails? Todo reappears with error message.

useSWRMutation: More Elegant Syntax

SWR 2.0 introduced dedicated useSWRMutation Hook with cleaner code:

import useSWRMutation from 'swr/mutation';

async function updateUser(url, { arg }) {
  await fetch(url, {
    method: 'POST',
    body: JSON.stringify(arg)
  });
}

function Profile() {
  const { trigger, isMutating } = useSWRMutation('/api/user', updateUser);

  return (
    <button
      onClick={() => trigger({ name: 'John' })}
      disabled={isMutating}
    >
      Update Name
    </button>
  );
}

trigger triggers mutations, isMutating tells you if request is in progress. Way more comfortable than manually managing loading states.

When Should You Use Optimistic Updates?

Not all operations suit optimistic updates. Rule of thumb:

Good for Optimistic Updates:

  • Like/favorite (low failure rate, reversible)
  • Add/delete list items (immediate result display more important)
  • Toggle state switches (like notification settings)
  • Form input saves (draft feature)

Bad for Optimistic Updates:

  • Payment operations (must wait for confirmation)
  • Delete account (irreversible, must be cautious)
  • Sensitive data modification (passwords, permissions)
  • Operations requiring server computation (like “generate report”, you don’t know what result looks like)

The principle: low failure rate, reversible, UX-prioritized scenarios—use optimistic updates. Critical business logic, irreversible operations—wait honestly for server response.

Let me share a mistake I made. Previously built a comments feature with optimistic updates. Users post comments, immediately displayed, great experience. Then one day found a user spamming comments—because their network was slow, after clicking “send” nothing happened (actually sent), so they clicked a dozen times. Each click optimistically updated, so the page showed a dozen identical comments. Though backend API had anti-duplicate logic, frontend UI still got messed up.

Later added isMutating check, disabling button during request, problem solved. Lesson: optimistic updates are great, but consider edge cases.

SWR vs React Query: How to Choose?

After all this about SWR’s benefits, you might ask: what about React Query? Search “React data fetching” online, tons of React Query articles too. Which to choose?

Honestly, both libraries are excellent and actively maintained in 2025. Wrong choice won’t kill you, but right choice makes development more comfortable.

Size Comparison: SWR is Lighter

  • SWR: 5.3KB (gzipped)
  • React Query (TanStack Query): 16.2KB

If you’re working on a bundle-size-sensitive project (like marketing landing pages, mobile H5), SWR has clear advantage. But honestly, for most projects, this 10KB difference isn’t that critical.

Complexity Comparison: SWR is Simpler

SWR’s API design is minimal, core is one useSWR Hook. Learn in 5 minutes, hands-on in 10.

React Query is more powerful but with steeper learning curve. Need to understand QueryClient, useQuery, useMutation, queryKeys, cache time vs stale time concepts. Long documentation.

If team tech stack is newer (or frontend experience limited), SWR is friendlier.

Feature Comparison: React Query is More Comprehensive

React Query has several features SWR doesn’t:

  1. Official DevTools: Visualize all query states, cache contents, refetch cycles. Amazing debugging experience. SWR has no official DevTools.

  2. Finer Cache Control: Can separately set cache time (how long data stays in memory) and stale time (how long until data goes stale). SWR is relatively simple, just cache+revalidate.

  3. Paginated/Infinite Queries: React Query has dedicated useInfiniteQuery, SWR has useSWRInfinite. Features similar, but React Query’s API is more mature.

  4. Stronger Mutation Support: React Query’s useMutation is more systematic with global mutation state, retry strategies, onSettled callbacks. SWR’s useSWRMutation was added in 2.0, features relatively basic.

Community and Ecosystem

React Query (now TanStack Query) has bigger community, more NPM weekly downloads. When you hit issues, easier to find answers on StackOverflow.

SWR is Vercel’s product, same family as Next.js. Official docs have dedicated Next.js integration guides. If using Next.js, SWR is the “first-party child.”

My Selection Suggestions

Choose SWRChoose React Query
Project relatively simple, data fetching logic not complexComplex project needing fine-grained cache control
Bundle size sensitive (mobile)Don’t care about extra 10KB
Using Next.jsUsing other frameworks (CRA, Vite, etc.)
Team has less frontend experienceTeam familiar with complex libraries, likes rich features
Want quick startWilling to spend time learning complete ecosystem

My own experience? Small projects or MVP stage, go SWR first—simple and fast. When project grows and requirements get complex, might migrate to React Query. But honestly, most projects SWR is totally sufficient.

One more point: migration cost between them isn’t actually high. Core concepts are similar (cache, revalidate, mutation). If you someday really need React Query’s advanced features, switching over won’t be too painful. Don’t overthink it.

Next.js and SWR: A Match Made in Heaven

SWR and Next.js are both Vercel products, naturally pair well. If using Next.js, there are several particularly convenient integration points.

Using SWR in App Router

Next.js 13+ App Router defaults to Server Components. SWR is a client library, so needs to be used in Client Components:

'use client';  // Must mark as client component

import useSWR from 'swr';

export default function Profile() {
  const { data } = useSWR('/api/user', fetcher);
  return <div>{data?.name}</div>;
}

Remember to add 'use client'. Newbies commonly forget this.

SSR + SWR: Pairing with Fallback Data

Next.js’s strength is Server-Side Rendering (SSR). You can fetch initial data in getStaticProps or getServerSideProps, then pass to SWR as fallback:

// pages/profile.js
export async function getStaticProps() {
  const user = await fetch('https://api.example.com/user').then(r => r.json());

  return {
    props: {
      fallback: {
        '/api/user': user  // key matches SWR's key
      }
    },
    revalidate: 60  // ISR: regenerate every 60 seconds
  }
}

export default function Profile({ fallback }) {
  return (
    <SWRConfig value={{ fallback }}>
      <UserProfile />
    </SWRConfig>
  );
}

function UserProfile() {
  // First render uses fallback data, no loading needed
  const { data } = useSWR('/api/user', fetcher);
  return <div>{data.name}</div>;
}

This way users’ first visit sees complete content (SEO-friendly), while SWR takes over on client-side and continues to revalidate. Best of both worlds.

Prefetching

For important data, can preload even before users click:

import { mutate } from 'swr';

function ArticleLink({ id }) {
  const prefetch = () => {
    mutate(`/api/article/${id}`, fetch(`/api/article/${id}`).then(r => r.json()));
  };

  return (
    <Link href={`/article/${id}`} onMouseEnter={prefetch}>
      Read more
    </Link>
  );
}

When user hovers mouse over link, starts loading article data. By the time they click, data might already be in cache, page opens instantly.

Infinite Scroll: useSWRInfinite

List pages’ most common need—scroll to load more:

import useSWRInfinite from 'swr/infinite';

function ArticleList() {
  const getKey = (pageIndex, previousPageData) => {
    if (previousPageData && !previousPageData.length) return null; // No more
    return `/api/articles?page=${pageIndex + 1}&limit=10`;
  };

  const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher);

  const articles = data ? data.flat() : [];
  const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined');

  return (
    <div>
      {articles.map(article => (
        <div key={article.id}>{article.title}</div>
      ))}
      <button
        onClick={() => setSize(size + 1)}
        disabled={isLoadingMore}
      >
        {isLoadingMore ? 'Loading...' : 'Load More'}
      </button>
    </div>
  );
}

setSize(size + 1) loads next page. SWR caches each page’s data, users scrolling back don’t need to reload.

Performance Optimization Tips

Using SWR in Next.js, few optimizations to note:

  1. Reasonably Set Revalidate Frequency

    • Static content (docs, help): disable auto-revalidate
    • User-related data: keep default focus revalidate
    • Real-time data: use refreshInterval polling
  2. API Route Caching

    • Next.js API routes can pair with Cache-Control headers
    • SWR respects HTTP cache headers
  3. Avoid Waterfall Requests

    • If multiple data have dependencies, consider merging requests server-side
    • Or use Next.js parallel data fetching

Let me share a real case. Previously built a blog site, article list page used SWR + ISR (Incremental Static Regeneration). First screen is statically generated HTML (instant), while users see content SWR checks in background for new articles, auto-updates list if any. Experience super smooth, and SEO perfect.

That’s the magic of Next.js + SWR—static site performance + dynamic app real-time updates.

Conclusion

After all that, let’s recap.

SWR solved React data fetching’s three pain points: code redundancy, lack of caching, multi-component sync. Core strategy “stale-while-revalidate” is both fast (instant cache display) and accurate (background updates). Add auto-revalidation, request deduping, optimistic updates—covers 90% of data fetching scenarios.

If I had to summarize best practices:

  1. Adjust Cache Strategy Based on Data Characteristics: Real-time data polls, stable data long cache
  2. Use Optimistic Updates for User Actions: Prerequisite is low failure rate, reversible
  3. Leverage Keys for Data Sharing: Don’t introduce state management just for sharing data
  4. Prioritize SWR for Next.js Projects: Pair with fallback data for SSR
  5. Simple Projects Choose SWR, Complex Choose React Query: Don’t over-engineer

Let me be real: SWR isn’t a silver bullet. It can’t fix bad API design or replace proper architecture. But if your backend APIs are already well-designed, SWR can make frontend code so much more elegant.

Next project, try SWR? Trust me, after using it you’ll come back to thank me.

FAQ

What is SWR's stale-while-revalidate strategy?
SWR's core concept:
• Show cached data immediately (fast response)
• Fetch fresh data in background (accuracy)
• Swap when new data arrives (seamless update)

This provides the best of both worlds: fast initial load and always fresh data.
What are SWR's main advantages?
SWR advantages:
• Automatic caching and revalidation
• Deduplication (same key only fetches once)
• Revalidation on focus/reconnect
• Optimistic updates support
• Error retry mechanism
• TypeScript support

Reduces code by 80% compared to manual data fetching.
How do I configure SWR caching?
Use SWRConfig for global configuration:
• revalidateOnFocus: revalidate when window gets focus
• revalidateOnReconnect: revalidate when network reconnects
• dedupingInterval: deduplication time window

Use mutate() for manual cache updates.
When should I use SWR vs Server Components?
Use SWR when:
• Client Components need data
• Need real-time updates
• Multiple components share data
• Need optimistic updates

Use Server Components when:
• Data can be fetched on server
• Don't need real-time updates
• Want better performance and SEO
How do I implement optimistic updates with SWR?
Use mutate with optimisticData:

Example:
mutate('/api/users', updateUsers(newUser), {
optimisticData: [...users, newUser],
rollbackOnError: true
})

This updates UI immediately, then syncs with server.
What's the difference between SWR and React Query?
SWR:
• Simpler API, easier to learn
• Smaller bundle size
• Good for most use cases

React Query:
• More features (mutations, infinite queries, etc.)
• Better for complex scenarios
• Larger bundle size

For Next.js projects, SWR is usually sufficient.
How do I use SWR with Next.js SSR?
Use fallback data with getServerSideProps or getStaticProps:

Example:
export async function getServerSideProps() {
const data = await fetchData()
return { props: { fallback: { '/api/users': data } } }
}

Then pass to SWRConfigProvider.

16 min read · Published on: Dec 19, 2025 · Modified on: Jan 22, 2026

Comments

Sign in with GitHub to leave a comment

Related Posts