React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

swrrevalidationfocusintervalmanual

Revalidation Strategies

Recipe

SWR provides multiple revalidation strategies: on window focus, on interval, on reconnect, and manual. Configure them globally or per-hook to keep data fresh without over-fetching.

"use client";
 
import useSWR from "swr";
 
const fetcher = (url: string) => fetch(url).then((r) => r.json());
 
function StockPrice({ symbol }: { symbol: string }) {
  const { data } = useSWR(`/api/stocks/${symbol}`, fetcher, {
    refreshInterval: 5000,         // Poll every 5 seconds
    revalidateOnFocus: true,       // Refresh on tab focus
    revalidateOnReconnect: true,   // Refresh on network recovery
    refreshWhenHidden: false,      // Pause when tab is hidden
    refreshWhenOffline: false,     // Pause when offline
  });
 
  return <span>${data?.price}</span>;
}

Working Example

"use client";
 
import useSWR, { useSWRConfig } from "swr";
import { useState } from "react";
 
interface Notification {
  id: string;
  message: string;
  read: boolean;
}
 
const fetcher = (url: string): Promise<Notification[]> =>
  fetch(url).then((r) => r.json());
 
export default function NotificationCenter() {
  const { data: notifications, mutate } = useSWR<Notification[]>(
    "/api/notifications",
    fetcher,
    {
      refreshInterval: 30000,      // Auto-refresh every 30s
      revalidateOnFocus: true,     // Check on tab return
    }
  );
 
  const handleManualRefresh = () => {
    mutate(); // Trigger manual revalidation
  };
 
  const handleMarkAllRead = async () => {
    await fetch("/api/notifications/mark-read", { method: "POST" });
    mutate(); // Revalidate after mutation
  };
 
  return (
    <div>
      <div className="flex justify-between">
        <h2>Notifications ({notifications?.filter((n) => !n.read).length ?? 0} unread)</h2>
        <button onClick={handleManualRefresh}>Refresh</button>
      </div>
      <ul>
        {notifications?.map((n) => (
          <li key={n.id} className={n.read ? "opacity-50" : ""}>
            {n.message}
          </li>
        ))}
      </ul>
      <button onClick={handleMarkAllRead}>Mark all as read</button>
    </div>
  );
}

Deep Dive

How It Works

  • Focus revalidation fires when the user switches back to the browser tab. SWR listens to the visibilitychange and focus events.
  • Interval revalidation sets up a setInterval that fires every refreshInterval ms. The timer pauses when the tab is hidden unless refreshWhenHidden is true.
  • Reconnect revalidation fires when the browser comes back online (the online event).
  • Manual revalidation is done by calling mutate() (bound) or mutate(key) (global) without arguments.
  • All revalidation strategies are deduplicated. If multiple components request the same key, only one fetch runs.
  • Stale data is shown immediately while the revalidation runs in the background.

Variations

Disable focus revalidation globally:

<SWRConfig value={{ revalidateOnFocus: false }}>
  {children}
</SWRConfig>

Conditional interval polling:

const { data } = useSWR("/api/job-status", fetcher, {
  // Only poll while the job is running
  refreshInterval: data?.status === "running" ? 1000 : 0,
});

Manual revalidation with global mutate:

import { mutate } from "swr";
 
// Revalidate a specific key from anywhere
mutate("/api/users");
 
// Revalidate all keys matching a filter
mutate(
  (key) => typeof key === "string" && key.startsWith("/api/"),
  undefined,
  { revalidate: true }
);

Revalidate on mount only:

const { data } = useSWR("/api/config", fetcher, {
  revalidateOnFocus: false,
  revalidateOnReconnect: false,
  revalidateIfStale: false,
  // Only fetches if no cached data exists
});

TypeScript Notes

  • Revalidation options are part of SWRConfiguration and fully typed.
  • refreshInterval accepts number or a function (latestData: Data) => number for dynamic intervals.
const { data } = useSWR<JobStatus>("/api/job", fetcher, {
  refreshInterval: (data) => (data?.complete ? 0 : 2000),
});

Gotchas

  • Focus revalidation can feel aggressive for data that rarely changes. Disable it for static-ish data like user profiles or app config.
  • refreshInterval: 0 disables interval polling (it is the default). Setting a very low interval (under 1000ms) can overload your API.
  • refreshWhenHidden: true keeps polling even when the user is on another tab. Use only when real-time updates matter even in background tabs.
  • Manual mutate() with no arguments revalidates by re-fetching. mutate(data) replaces cache data without re-fetching. These are very different operations.
  • When revalidateIfStale: false, SWR will not re-fetch on mount if there is cached data, even if that cached data is hours old.

Alternatives

ApproachProsCons
Focus revalidationAutomatic, no config neededMay fire too often for stable data
Interval pollingSimple, predictableWasteful if data rarely changes
WebSocket pushReal-time, server-drivenRequires WebSocket infrastructure
Server-Sent Events (SSE)Lightweight push, HTTP-basedOne-directional, limited browser connections
Manual revalidationFull controlRequires explicit trigger points

FAQs

What browser events trigger focus revalidation?

SWR listens to the visibilitychange and focus events. When the user switches back to the browser tab, SWR fires a background revalidation for all active keys.

How does interval revalidation behave when the tab is hidden?

By default, the polling timer pauses when the tab is hidden. Set refreshWhenHidden: true to continue polling in background tabs, but only do this when real-time updates matter even when the tab is not visible.

What is the difference between mutate() with no arguments and mutate(data)?
  • mutate() revalidates by re-fetching from the server.
  • mutate(data) replaces the cached data directly without re-fetching.
  • These are very different operations with different use cases.
How do I revalidate all keys that match a pattern?
import { mutate } from "swr";
 
mutate(
  (key) => typeof key === "string" && key.startsWith("/api/"),
  undefined,
  { revalidate: true }
);
What does revalidateIfStale: false do?

SWR will not re-fetch on mount if cached data already exists, even if that data is hours old. Use this for data that rarely changes (e.g., app config) combined with disabling focus and reconnect revalidation.

How can I implement conditional interval polling that stops when a job completes?
const { data } = useSWR("/api/job-status", fetcher, {
  refreshInterval: data?.status === "running" ? 1000 : 0,
});

Setting refreshInterval to 0 disables polling.

Gotcha: Why might focus revalidation feel too aggressive for some data?

Every time the user switches tabs and returns, SWR re-fetches. For stable data like user profiles or app config, this creates unnecessary requests. Disable it per-hook with revalidateOnFocus: false.

Gotcha: What happens with a very low refreshInterval like 100ms?

SWR will poll your API multiple times per second, which can overload your server and waste bandwidth. Keep intervals reasonable (1000ms minimum for most use cases).

How is refreshInterval typed in TypeScript?

It accepts number or a function (latestData: Data) => number for dynamic intervals:

const { data } = useSWR<JobStatus>("/api/job", fetcher, {
  refreshInterval: (data) => (data?.complete ? 0 : 2000),
});
Are revalidations from different components using the same key deduplicated?

Yes. All revalidation strategies are deduplicated. If multiple components use the same key, only one network request runs regardless of how many components trigger revalidation.