Using Cache
Every policy is opt-in per call site through the cache option; cachePlugin() only sets the defaults. A read or action with no cache option behaves exactly as it does without the pack.
This guide assumes the plugin is registered — see Installation. Examples build on the data-loading primitives (useData, useAction, .match()).
Freshness: staleTime
staleTime is how long a cached value is considered fresh. Within the window the value is served with no fetch. Past it, the stale value is served immediately and revalidated in the background — the read moves to state refreshing, so the ready arm keeps rendering while the new value loads:
import { component, useData } from 'sigx';
const Profile = component(() => {
const user = useData('user', fetchUser, {
cache: { staleTime: 60_000 }, // fresh for a minute
});
return () =>
user.match({
pending: () => <Spinner />,
error: (e) => <p>{e.message}</p>,
ready: (u) => <h1>{u.name}</h1>, // stays on screen during background revalidation
});
});
The default is 0 — every mount revalidates. Raise it for data that doesn't change often; a value that never goes stale within a session can use a very large staleTime.
Retention: gcTime
When the last component reading a key unmounts, the entry isn't dropped straight away — it's retained for gcTime (default 5 minutes). Navigate away and back within that window and the value is there instantly, no refetch:
const list = useData('inbox', fetchInbox, {
cache: { staleTime: 30_000, gcTime: 600_000 }, // keep for 10 minutes
});
Set gcTime: 0 to drop an entry the moment nothing is reading it.
Revalidation: on focus, on an interval
Mounted reads can refetch on their own. revalidateOnFocus refetches when the window regains focus or becomes visible again; revalidateOnInterval refetches on a timer:
const feed = useData('feed', fetchFeed, {
cache: {
staleTime: 15_000,
revalidateOnFocus: true,
revalidateOnInterval: 30_000, // ms
},
});
Both respect staleTime — a value still fresh isn't refetched. On the web, focus revalidation is wired to window focus and visibilitychange out of the box; on other platforms you supply the event source (see Renderer portability).
No skeleton flash: keepPreviousData
When a read's key changes and nothing is cached for the new key, core resets to pending. For pagination or a filter, that flashes a skeleton on every page. keepPreviousData keeps the previous key's value on screen (state refreshing) until the new one arrives:
const Results = component((ctx) => {
const page = ctx.signal(1);
const results = useData(
() => ['results', page.value],
([, p]) => fetchPage(p),
{ cache: { keepPreviousData: true } },
);
return () => (
<div>
{results.match({
pending: () => <Skeleton />, // only the very first load
error: (e) => <p>{e.message}</p>,
ready: (rows) => <Rows data={rows} dim={results.state === 'refreshing'} />,
})}
<button onClick={() => page.value++}>Next</button>
</div>
);
});
Invalidating after a write
invalidate() drops an entry's freshness and refetches it for every mounted consumer. Call it directly on a read, or — more usefully — declare it on the action that made the write. Action invalidates accepts exact keys or tuple prefixes: ['users'] matches every ['users', …] read.
const save = useAction(saveUser, {
cache: {
invalidates: [['users']], // prefix: refetch every ['users', …] read
},
});
// somewhere in a handler:
const r = await save.run(draft);
Calling invalidate() by hand works too, when the trigger isn't an action:
const user = useData('user', fetchUser, { cache: {} });
user.invalidate(); // refetch this read everywhere it's mounted
Optimistic updates: mutate() and optimistic
mutate() writes through the cache immediately — every mounted consumer re-renders with the new value before any request settles:
user.mutate((current) => ({ ...current, name: 'Ada' }));
For an action, declare the optimistic write inline. apply produces the provisional value from the current entry and the action input; if the run fails it rolls back — unless something newer wrote to the entry in the meantime:
const rename = useAction(renameUser, {
cache: {
optimistic: {
key: 'user',
apply: (current, next) => ({ ...current, name: next.name }),
},
invalidates: [['users']],
},
});
Renderer portability
The pack works on any renderer because it never imports the sigx umbrella. Two things are platform-specific.
Focus revalidation's event source. On the web the default subscribes to window focus and visibilitychange, installed only when a DOM is present. On another platform, hand the plugin your own attention event through revalidateTrigger — return an unsubscribe and it runs on app teardown:
app.use(cachePlugin({
revalidateTrigger: (revalidate) => {
const off = platform.onResume(revalidate); // lynx app-resume, terminal focus, …
return off;
},
}));
Reads still opt in per call site with revalidateOnFocus.
Live-client detection. A windowless client runtime must declare itself a live client once, from its platform module — declareLiveClient() from @sigx/runtime-core/internals. This is a renderer concern, not an app one; on the web nothing is required.
Precise typing
useData(...) still returns AsyncState<T>; the augmentation adds an optional invalidate?() and mutate?(). For a read you know is cached, assert the exported view to type them as present:
import type { CachedAsyncState } from '@sigx/cache';
const user = useData('user', fetchUser, { cache: {} }) as CachedAsyncState<User>;
user.invalidate(); // now typed non-optional
Next steps
- API reference — every export, option and default
- Data loading — the
useData/useActionprimitives this builds on
