React - Stale‑While‑Revalidate (SWR)
Overview
Estimated time: 20–30 minutes
Implement the stale‑while‑revalidate strategy: serve cached data immediately, kick off a background refresh, and update the UI when new data arrives.
Try it: SWR with background refresh
View source
const { useEffect, useRef, useState } = React;
const cache = new Map(); // url -> { data, ts }
async function fetchJSON(url){
const res = await fetch(url);
if (!res.ok) throw new Error('HTTP '+res.status);
return res.json();
}
function useSWR(url, { ttl=15000 }={}){
const [state, setState] = useState({ data:null, error:'', refreshing:false, from:'' });
const ctlRef = useRef(null);
useEffect(() => {
let canceled = false;
async function run(){
const hit = cache.get(url);
if (hit && Date.now()-hit.ts < ttl){
setState({ data:hit.data, error:'', refreshing:true, from:'cache' });
// Background refresh
try {
const fresh = await fetchJSON(url);
if (!canceled){ cache.set(url, { data:fresh, ts:Date.now() }); setState({ data:fresh, error:'', refreshing:false, from:'network' }); }
} catch (e){ if (!canceled) setState(s => ({ ...s, refreshing:false })); }
} else {
setState(s => ({ ...s, refreshing:true }));
try {
const data = await fetchJSON(url);
if (!canceled){ cache.set(url, { data, ts:Date.now() }); setState({ data, error:'', refreshing:false, from:'network' }); }
} catch (e){ if (!canceled) setState({ data:null, error:e.message||'Error', refreshing:false, from:'' }); }
}
}
if (url) run();
return () => { canceled = true; };
}, [url, ttl]);
function invalidate(){ cache.delete(url); }
return { ...state, invalidate };
}
function Demo(){
const [id, setId] = useState('1');
const url = id ? `https://jsonplaceholder.typicode.com/posts/${id}` : '';
const { data, error, refreshing, from, invalidate } = useSWR(url, { ttl: 15000 });
return (
<div>
<label>Post ID: <input value={id} onChange={e => setId(e.target.value)} style={{width:80}} /></label>
<button onClick={invalidate} style={{marginLeft:8}}>Invalidate</button>
{error && <div style={{color:'salmon'}}>{error}</div>}
{data && <div style={{marginTop:8}}>
<div style={{color:'var(--muted)'}}>From: {from} {refreshing && '(refreshing...)'}</div>
<h4 style={{margin:'8px 0'}}>{data.title}</h4>
<div>{data.body}</div>
</div>}
</div>
);
}
ReactDOM.createRoot(document.getElementById('try-swr')).render(<Demo />);
Syntax primer
- Use an in-memory cache with timestamps and TTL.
- When a cache hit is fresh, render it and refresh in the background.
- Indicate refresh status to users and update when new data arrives.
Common pitfalls
- Not indicating background refresh – users think data is stale.
- Never invalidating – provide a manual Invalidate or time-based revalidation.
Checks for Understanding
- What are the core steps in a stale‑while‑revalidate flow?
- How do you signal to users that new data is loading in the background?
Show answers
- Serve cached data if fresh, start a background fetch, and update cache/UI on completion.
- Show a small indicator (e.g., “refreshing…”) or a spinner near the data.
Exercises
- Add per‑key TTL and a global invalidate‑all action.
- Handle fetch cancellation on rapid ID changes to avoid race conditions.
- Persist cache to
localStorage
and restore on page load.