Caching
How What Framework caches server data, deduplicates requests, and keeps your UI fresh.
Stale-While-Revalidate
What's data fetching is built on the SWR pattern: show cached (stale) data immediately, then revalidate in the background and update when fresh data arrives. This means your UI never shows a loading spinner for data it's already fetched.
import { useSWR } from 'what-framework';
function UserProfile({ userId }) {
const { data, error, isLoading, isValidating } = useSWR(
`/api/users/${userId}`,
(url) => fetch(url).then(r => r.json())
);
// First load: isLoading=true, data=null
// After fetch: isLoading=false, data={...}
// On revisit: data shows instantly (cached), isValidating=true in background
// When fresh data arrives: data updates seamlessly
if (isLoading()) return <p>Loading...</p>;
if (error()) return <p>Error: {error().message}</p>;
return (
<div>
<h1>{data().name}</h1>
{isValidating() && <span class="badge">Refreshing...</span>}
</div>
);
}
Signal Getters
data(), error(), isLoading(), and isValidating() are all signal getters — call them with parentheses to read the current value.
Shared Cache
The cache is global and keyed by the first argument to useSWR. Multiple components reading the same key share one cache entry and one network request:
// Both components read from the same cache entry
function Header() {
const { data } = useSWR('/api/user', fetchUser);
return <span>Hi, {data()?.name}</span>;
}
function Sidebar() {
const { data } = useSWR('/api/user', fetchUser);
return <img src={data()?.avatar} />;
}
// Only ONE fetch request is made, and both components update together
Request Deduplication
If multiple components mount at the same time and request the same key, What deduplicates the requests. Only one fetch runs; all callers share the result. The deduplication window defaults to 2 seconds:
const { data } = useSWR('/api/posts', fetcher, {
dedupingInterval: 5000, // 5s dedup window (default: 2000ms)
});
Within the dedup window, calling useSWR with the same key returns the in-flight promise instead of starting a new request.
Automatic Revalidation
What automatically revalidates cached data in several scenarios:
const { data } = useSWR('/api/posts', fetcher, {
revalidateOnFocus: true, // Re-fetch when tab regains focus (default)
revalidateOnReconnect: true, // Re-fetch when network reconnects (default)
refreshInterval: 30000, // Poll every 30s (0 = disabled, default)
dedupingInterval: 2000, // Min time between fetches (default)
});
- Focus revalidation — When the user switches back to your tab, all active queries re-fetch. Stale data is shown while fresh data loads.
- Reconnect revalidation — When the browser comes back online after losing connection, data is refreshed.
- Polling — Set
refreshIntervalfor data that changes frequently (dashboards, feeds, stock prices).
Cache Invalidation
After a mutation (create, update, delete), invalidate related queries to trigger a re-fetch:
import { invalidateQueries } from 'what-framework';
async function createPost(data) {
await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(data),
});
// Soft invalidation (default): keeps stale data visible, re-fetches in background
invalidateQueries('/api/posts');
}
// Hard invalidation: clears data immediately, shows loading state
invalidateQueries('/api/posts', { hard: true });
// Invalidate multiple keys with a predicate
invalidateQueries(key => key.startsWith('/api/posts'));
Soft vs Hard
Soft invalidation (default) keeps stale data on screen while re-fetching — the SWR pattern. Hard invalidation clears the cache entry immediately, causing isLoading() to become true and data to be null until the fresh response arrives.
Manual Cache Control
Read and write cache entries directly for advanced scenarios:
import { setQueryData, getQueryData, clearCache } from 'what-framework';
// Read from cache without triggering a fetch
const cached = getQueryData('/api/user');
// Write to cache directly (updates all subscribers)
setQueryData('/api/user', { name: 'Alice', role: 'admin' });
// Update cache based on current value
setQueryData('/api/posts', posts =>
posts.map(p => p.id === 42 ? { ...p, title: 'Updated' } : p)
);
// Clear entire cache (useful for logout)
clearCache();
Optimistic Updates
Update the UI immediately before the server responds. The mutate function from useSWR lets you set local data that will be replaced when revalidation completes:
function TodoList() {
const { data, mutate, revalidate } = useSWR('/api/todos', fetcher);
async function toggleTodo(id) {
// 1. Optimistically update the cache
mutate(todos =>
todos.map(t => t.id === id ? { ...t, done: !t.done } : t)
);
// 2. Send to server
await fetch(`/api/todos/${id}/toggle`, { method: 'POST' });
// 3. Revalidate to get server truth
revalidate();
}
return (
<ul>
{data()?.map(todo =>
<li onClick={() => toggleTodo(todo.id)}>
{todo.done ? '✓' : '○'} {todo.text}
</li>
)}
</ul>
);
}
Prefetching
Pre-populate the cache before a component mounts. Useful for hover-to-prefetch patterns:
import { prefetchQuery } from 'what-framework';
function PostLink({ id, title }) {
return (
<a
href={`/posts/${id}`}
onMouseenter={() => prefetchQuery(
`/api/posts/${id}`,
(url) => fetch(url).then(r => r.json())
)}
>
{title}
</a>
);
}
When the user hovers, the data is fetched and cached. If they click through, useSWR returns the cached data instantly — no loading spinner.
Conditional Fetching
Pass null, undefined, or false as the key to pause fetching. Useful for dependent queries where one fetch depends on another's result:
function UserPosts({ userId }) {
const user = useSWR(
`/api/users/${userId}`,
fetcher
);
// Only fetch posts after user data is available
const posts = useSWR(
user.data() ? `/api/users/${user.data().id}/posts` : null,
fetcher
);
return (
<div>
<h1>{user.data()?.name}</h1>
{posts.data()?.map(p => <p>{p.title}</p>)}
</div>
);
}
Cache Size & Eviction
The global cache holds up to 200 entries. When the limit is exceeded, the oldest 20% of entries are evicted (LRU policy). Entries with active subscribers are never evicted.
To customize, clear stale data on logout or route transitions:
import { clearCache } from 'what-framework';
function logout() {
clearCache(); // Remove all cached data
navigate('/login'); // Redirect to login
}
Fallback & Placeholder Data
Provide initial data to show before the first fetch completes:
const { data } = useSWR('/api/settings', fetcher, {
fallbackData: { theme: 'light', lang: 'en' },
});
// data() returns fallbackData immediately, then updates with server data
Error Handling
Errors are captured and available via the error() signal. Use the onError and onSuccess callbacks for side effects:
const { data, error, revalidate } = useSWR('/api/data', fetcher, {
onError: (err, key) => {
console.error(`Failed to fetch ${key}:`, err);
toast.error('Something went wrong');
},
onSuccess: (result, key) => {
console.log(`Fetched ${key}:`, result);
},
});
// Retry on error
if (error()) {
return (
<div>
<p>Error: {error().message}</p>
<button onClick={revalidate}>Retry</button>
</div>
);
}