Revalidation and Rendering Gotchas

Purpose and Scope

This page helps diagnose cases where an SWR-powered component appears to refetch unexpectedly, render more often than expected, skip a refetch, retry in an unfamiliar way, or preserve old data during a React transition. In SWR, revalidation means asking the fetcher for fresh data after cached or existing state has already been used. Rendering symptoms often come from the same causes as revalidation symptoms: focus events, online events, polling timers, deduplication windows, Suspense error recovery, and React concurrent rendering all affect when state changes become visible.

Sources: test/use-swr-focus.test.tsx, test/use-swr-offline.test.tsx, test/use-swr-refresh.test.tsx, e2e/test/concurrent-transition.test.ts, e2e/site/app/render-count/page.tsx, e2e/site/component/manual-retry.tsx, e2e/site/component/manual-retry-mutate.tsx

The most important troubleshooting habit is to separate a request trigger from a render trigger. A focus event can trigger a revalidation, but a render only changes if SWR receives new data, error, validation, or loading state. A polling timer can wake up frequently, yet dedupingInterval can prevent a new fetch from running during the dedupe window. In concurrent rendering, React can intentionally keep showing the previous result while a transition is pending. These behaviors are deliberate, and the tests in this repository encode them as compatibility expectations rather than incidental implementation details.

Relevant Source Files

  • test/use-swr-focus.test.tsx exercises focus-driven revalidation, revalidateOnFocus, focusThrottleInterval, and stateful changes to those options.
  • test/use-swr-offline.test.tsx verifies that SWR avoids revalidating while offline and revalidates when the window becomes online.
  • test/use-swr-refresh.test.tsx covers refreshInterval, interaction with dedupingInterval, and changing interval values over time.
  • e2e/test/concurrent-transition.test.ts checks that changing a key inside a React transition keeps old data visible while the transition is pending.
  • e2e/test/perf.test.ts measures the time from a state change to painting an expensive component and asserts it stays under one second.
  • e2e/site/app/render-count/page.tsx is a minimal page used to detect whether a bare useSWR call causes extra rerenders.
  • e2e/site/component/manual-retry.tsx demonstrates Suspense plus an error boundary where retrying resets the boundary and preloads remote data.
  • e2e/site/component/manual-retry-mutate.tsx demonstrates a manual retry path that calls mutate(key, fetcher) before resetting the error boundary.

Focus and Visibility Revalidation

By default, SWR revalidates on focus. The focus tests mount a component with a counter fetcher, wait for initial data, simulate a window focus, and expect the counter-backed data to advance. If your application appears to refetch after switching tabs, clicking back into the browser, or returning from another app, first check whether revalidateOnFocus is enabled for that hook or inherited from configuration. The tests also set dedupingInterval: 0 and focusThrottleInterval: 0 when they want every simulated focus to produce an observable result, which is a useful diagnostic pattern when reproducing focus behavior locally.

Sources: test/use-swr-focus.test.tsx

When focus revalidation is unwanted, set revalidateOnFocus: false for the hook or at the appropriate configuration scope. The focus test suite verifies that a hook with this option remains at the original value after a focus event. It also verifies that revalidateOnFocus is stateful: a component can toggle the option from false to true, then focus events start revalidating, and toggle it back to false to stop future focus-triggered updates. This matters when a feature flag, modal state, route state, or authentication state changes revalidation policy during the component lifetime.

focusThrottleInterval is the second focus-related setting to inspect. The tests show a hook with focusThrottleInterval: 50 ignoring a focus event that happens within the throttling interval and accepting a later focus after enough time passes. If a user says, “focus sometimes refetches and sometimes does not,” the answer may not be random behavior; it may be throttling. During debugging, log both the time of the focus event and the last successful revalidation, then compare the difference with the configured throttle interval and any global defaults.

Offline, Online, and Retry Behavior

Network state changes are another common source of surprising revalidation behavior. The offline tests dispatch browser offline and online events directly. After the initial data load, the test that goes offline triggers a focus event and expects the displayed data to remain unchanged. The complementary test dispatches online and expects an immediate revalidation. In an application, this means that focus alone is not enough to explain a fetch; SWR also consults browser connectivity signals, and reconnecting can be the event that causes data to refresh.

Sources: test/use-swr-offline.test.tsx

Manual retry behavior is clearest in the E2E Suspense examples. One retry component renders useRemoteData() inside Suspense and ErrorBoundary; when the fallback button is clicked, the boundary resets and preloadRemote() is called from the reset handler. A second component exports a fetcher that intentionally rejects the first call, then fetches /api/retry on the next attempt. Its fallback button awaits mutate(key, fetcher) and then resets the boundary. If your retry UI is stuck on an error fallback, verify whether the retry only resets React error state or also repopulates the SWR cache by preloading or mutating.

Sources: e2e/site/component/manual-retry.tsx, e2e/site/component/manual-retry-mutate.tsx

A practical retry checklist is: confirm the key used by the failed hook, confirm the fetcher used by retry, decide whether the retry should call mutate(key, fetcher), and reset the error boundary only after the cache has been updated or the next request has been scheduled. In Suspense mode, thrown promises and thrown errors are part of rendering, so retry code often has to coordinate React’s error boundary lifecycle with SWR’s cache lifecycle. Treat the boundary reset and the SWR retry as two separate actions, even if the user sees only one Retry button.

Interval Revalidation and Deduplication

Polling issues usually come from the interaction between refreshInterval and dedupingInterval. The refresh tests show a hook with refreshInterval: 200 and dedupingInterval: 100 advancing after the 200 ms timer, skipping an intermediate 50 ms advance, and then updating again after the next full interval. Another test sets refreshInterval: 100 with dedupingInterval: 500 and shows that the 100 ms timer does not automatically mean a new request every 100 ms. SWR dedupes requests until the deduping interval has elapsed, so several timer ticks can intentionally reuse the same in-flight or recent result.

Sources: test/use-swr-refresh.test.tsx

If interval revalidation feels inconsistent after component state changes, inspect whether the interval value itself is changing. The refresh tests include a component whose refreshInterval is stored in React state and changes after clicks. The expected behavior is that SWR clears and recreates timers as interval values change, including stopping refresh when the interval becomes zero. This is especially relevant for dashboards that slow down polling when hidden, pause polling after an error, or let the user choose a refresh cadence. A stale closure in surrounding component code can look like an SWR timer bug, so log the effective option value rendered with the hook.

Concurrent Rendering and Render Counts

React concurrent transitions can make key changes look delayed even when SWR is behaving correctly. The Playwright transition test navigates to a concurrent-transition page, confirms initial data for initial-key, clicks a transition trigger, observes isPending:1, and expects the old data:initial-key content to remain visible while the transition is pending. Only after isPending returns to zero does the test expect data:new-key. The troubleshooting conclusion is that old data during a transition is not necessarily stale cache leakage; it can be React preserving the previous UI until the transition completes.

Sources: e2e/test/concurrent-transition.test.ts

Render-count and performance tests provide guardrails for diagnosing rerender complaints. The render-count page simply calls useSWR('swr should not cause extra rerenders'), logs console.count('render'), and renders static text. That page exists to catch unnecessary rerenders in a minimal case. The performance E2E test clicks a checkbox, waits for an expensive component with 10,000 child nodes to paint, records performance marks, and asserts the render completes under one second. When debugging local performance, use the same style of measurement: mark the user event, wait for the visible result, and measure to paint rather than relying only on console log ordering.

Sources: e2e/site/app/render-count/page.tsx, e2e/test/perf.test.ts

Diagnostic Flow

Start with the hook key and options. If the key changes, determine whether the change happens inside a transition and whether React is expected to keep showing the previous result while pending. If the key is stable, list every enabled revalidation trigger: focus, reconnect, interval polling, manual mutate, preload, and retry UI. Then compare observed timing with dedupingInterval, focusThrottleInterval, and any dynamic refreshInterval value. This turns a vague symptom such as “SWR refetches randomly” into a small set of concrete events and intervals that can be reproduced.

For local reproduction, temporarily use a fetcher that increments a counter and render the counter in the DOM, mirroring the unit tests. Set dedupingInterval: 0 only when you need to prove that a trigger fires; restore the real deduping behavior afterward because production deduplication is part of the contract. For retry and Suspense issues, make the first fetch reject and the second fetch resolve, then test whether your retry button updates the SWR cache, resets the error boundary, or does both in the right order.

Next Steps

If the symptom is about a specific API surface, read the useSWR, SWRConfig, mutate, and preload reference pages next. If the symptom appears only in examples or app code, compare it with the focus revalidation, interval refetching, server rendering, Suspense, and optimistic mutation recipes. When filing an issue or adding a regression test, include the key, fetcher behavior, relevant options, browser event or timer sequence, and whether React Suspense or concurrent transitions are involved.