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>;
}
datais the action's result, ornulluntil the first load.isLoadingis true while a request is in flight;isError/errorcarry a failure.enabled: falseturns the fetch off entirely — the idiomatic way to wait for inputs.ttlcontrols 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 theDataClient) marks an entry stale and re-runs it — and accepts aRegExpto 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.