Tutorial

How to Fetch with SWR Stale While Revalidate

Use SWR to serve stale data immediately and revalidate in the background, so the UI feels instant and the data stays fresh without blocking.

On this page

  1. Introduction
  2. Configure the hook
  3. Set dedupingInterval
  4. Handle focus revalidation
  5. Optimistic update
  6. Roll back on error
  7. Test SWR
  8. Common pitfalls
  9. Conclusion

Introduction

The hardest part of show stale while refetching is not writing the code — it is deciding what the code is allowed to reject. A SWR setup that accepts everything is easy to ship and expensive to operate, because every malformed input becomes a production surprise. The discipline is to enumerate the shapes the system actually receives, encode them in SWR, and let the library reject the rest loudly. This keeps the happy path fast and the failure path visible.

Choosing SWR for show stale while refetching is a trade, not a winner. You give up some flexibility to get a hard boundary, and the exchange pays off when the boundary catches a class of bugs that would otherwise surface at 3 a.m. Document the trade at the call site so the next reader does not undo the guard thinking it is a bug. The reader who does not know why the guard exists is the reader who removes it.

The sections below walk through the concrete decisions: where to put the swr check, how to keep it fast under load, what to do when the contract changes, and how to test the failure paths so they degrade on purpose instead of by accident.

Configure the hook

Configure the hook is the first decision because it determines what the rest of the SWR pipeline can assume. A wrong choice here turns into show stale while refetching under load, and the invariant you thought SWR guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Configure the hook is the one most teams skip: drive swr to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

import useSWR from 'swr';
const { data, error, mutate } = useSWR('/api/user', fetcher, {
  revalidateOnFocus: true, dedupingInterval: 2000,
});

The typescript above is the shape to ship for show stale while refetching. It is small enough to audit and specific enough that the failure behavior is obvious. Adapt the constants to your traffic and latency budget, and resist the urge to handle cases the system has not seen yet.

Set dedupingInterval

The mechanism behind Set dedupingInterval is straightforward in SWR, but the consequences ripple. A wrong choice here turns into show stale while refetching under load, and the invariant you thought SWR guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Set dedupingInterval is the one most teams skip: drive swr to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Handle focus revalidation

Handle focus revalidation is where most SWR implementations leak performance. A wrong choice here turns into show stale while refetching under load, and the invariant you thought SWR guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Handle focus revalidation is the one most teams skip: drive swr to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

test('serves stale then fresh', async () => {
  render(<Comp/>); expect(screen.getByText('old')).toBeInTheDocument();
  await waitFor(() => screen.getByText('new'));
});

Pin the behavior with this test. A SWR setup that passes this test degrades on purpose instead of by accident, which is the difference between a managed failure and an incident.

Optimistic update

The hard part of Optimistic update is not the happy path — it is what happens when swr fails halfway. A wrong choice here turns into show stale while refetching under load, and the invariant you thought SWR guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Optimistic update is the one most teams skip: drive swr to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Roll back on error

Roll back on error should be observable without a debugger. A wrong choice here turns into show stale while refetching under load, and the invariant you thought SWR guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Roll back on error is the one most teams skip: drive swr to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Test SWR

Test SWR is a trade, not a winner. A wrong choice here turns into show stale while refetching under load, and the invariant you thought SWR guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Test SWR is the one most teams skip: drive swr to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Common pitfalls

The common pitfalls in show stale while refetching all reduce to one mistake: treating SWR as a formality instead of a contract. The contract is the thing the rest of the system relies on, and a weak contract propagates weakness everywhere.

  • Skipping the swr boundary so show stale while refetching enters unvalidated.
  • Assuming SWR is fast without measuring the tail latency under load.
  • Letting show stale while refetching fail silently instead of failing loudly with a metric.
  • Testing only the happy path so the swr failure surfaces in production.
  • Sharing mutable state across requests so one bad input poisons the next.

Conclusion

Do not conflate the conclusion with the implementation. They look similar from the outside but the failure modes differ: SWR fails fast and the caller knows, while an unchecked path fails slow and the caller guesses. Choose the one whose failure mode your system can absorb, and document the choice at the call site.

Self-check: write a test that drives swr to the failing condition and assert the system degrades gracefully. Then show stale while refetching is a property you maintain, not a feature you hope for.

Sources: SWR, Stale-while-revalidate

FAQ

When revalidate?
On focus, reconnect, and interval. Tune per key — not all data needs the same freshness.
Optimistic update?
Use mutate(data, false) to update without revalidating, then revalidate on settle.

Related Examples