Skip to main content

Fetching and caching data

useFetch from @archibald/client is the framework's data-fetching primitive: it runs your action on the server during SSR, transfers the result to the client, and caches it under a key. This recipe covers the everyday patterns; the full option surface lives in the useFetch API reference and its deep-dive pages.

Basics: key, action, options

The key is an array identifying the cache entry — include everything the request depends on, so a changed input produces a new key and triggers a fetch. The action returns the data (or a promise of it). Options tune caching and behavior:

import { useFetch } from '@archibald/client';

function ProductTeaser({ id }: { readonly id: string }) {
const { data, error, isLoading, refetch } = useFetch<Product>(
['product', id],
() => productService.getProduct(id),
{ ttl: 60_000, enabled: !!id }
);

if (isLoading) {
return <TeaserSkeleton />;
}

if (error) {
return <ErrorNote onRetry={() => refetch()} />;
}

return <h2>{data?.name}</h2>;
}
  • data is the action's result, or null until the first load.
  • isLoading is true while a request is in flight; isError/error carry a failure.
  • enabled: false turns the fetch off entirely — the idiomatic way to wait for inputs.
  • ttl controls how long the cached entry stays fresh.

The same object-style signature works too: useFetch({ key: ['product', id], data: () => …, ttl: 60_000 }). For Suspense-first code, see useSuspenseFetch.

refetch vs invalidate

  • refetch (returned by the hook) forces the request for this key to run again immediately, bypassing freshness.
  • invalidate (on the DataClient) marks an entry stale and re-runs it — and accepts a RegExp to hit many keys at once. Use it from other components after a write, when you know related data changed:
import { useDataClient } from '@archibald/client';

const dataClient = useDataClient();

await dataClient.invalidate(['product', id]); // one entry
await dataClient.invalidate(/^cart/); // every cart-related entry

Optimistic update with useMutation

For writes, pair useMutation with the DataClient cache. dataClient.get(key) returns the cached response payload (or null); write a new payload back with dataClient.set(key, { response: … }):

import { useDataClient, useMutation } from '@archibald/client';

function RenameWishlist({ id }: { readonly id: string }) {
const dataClient = useDataClient();
const { mutate, isPending } = useMutation();
const key = ['wishlist', id];

function rename(name: string) {
return mutate(async () => {
const previous = dataClient.get(key) as Wishlist | null;

// Show the new name immediately.
dataClient.set(key, { response: { ...previous, name } });

try {
return await wishlistService.rename(id, name);
} catch (error) {
// Roll back to what the server last confirmed.
dataClient.set(key, { response: previous });
throw error;
} finally {
// Reconcile with the server either way.
await dataClient.invalidate(key);
}
});
}

return <RenameForm onSubmit={rename} busy={isPending} />;
}

Every useFetch(['wishlist', id], …) subscriber re-renders with the optimistic value the moment set runs, and again when invalidate brings back the server's answer.