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 refreshInterval for 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>
  );
}