Overview

asyncAtom is a query-style resource: a fetcher plus reactive data, isLoading, error, and status atoms. Calling the asyncAtom reads data.

Pass keys (an array of atoms) to auto-run when those keys change. Omit or pass an empty keys array for manual-only mode — then only refetch() fetches.

The fetcher receives { zero } — a ZeroPromise with .signal / .cancel() for abort. Optional debounce delays auto-run; staleTime > 0 schedules quiet background refreshes after settle.

asyncAtom vs nearby APIs

API
Role
Prefer when
asyncAtom Resource Keyed/manual fetch with status — read like an atom.
asyncAction Callable workflow Invoke with args; cache/polling helpers. See Actions.
atom + manual fetch DIY Fine for one-off loads; you wire loading/error yourself.

Keyed auto-fetch

Changing the id key re-runs the fetcher. Simulated delay + error path.

import { html, atom, asyncAtom, renderApp } from 'mates';

const App = () => {
const id = atom('ada');

const profile = asyncAtom(async () => {
await new Promise((r) => setTimeout(r, 600));
if (id() === 'fail') throw new Error('User not found');
return { id: id(), name: id() === 'ada' ? 'Ada' : 'Grace' };
}, { default: null, keys: [id] });

return () => html`
<div class="m-col m-gap">
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-primary" @click=${() => id.set('ada')}>ada</button>
<button @click=${() => id.set('grace')}>grace</button>
<button class="btn-danger" @click=${() => id.set('fail')}>fail</button>
<button @click=${() => profile.refetch()}>refetch</button>
</div>
<p class="muted m-t-5">status=${profile.status()} · loading=${profile.isLoading()}</p>
${profile.isLoading()
? html`<p class="muted">Loading…</p>`
: profile.error()
? html`<p>${profile.error().message}</p>`
: profile()
? html`<p><strong>${profile().name}</strong> (${profile().id})</p>`
: html`<p class="muted">No data</p>`
}
</div>
`;
};
renderApp(App, document.getElementById('app'));

Manual refetch (no keys)

Omit keys for refetch-only resources. cancel() aborts the in-flight ZeroPromise.

import { html, asyncAtom, renderApp } from 'mates';

const App = () => {
// No keys → manual refetch only
const joke = asyncAtom(async () => {
await new Promise((r) => setTimeout(r, 500));
const lines = ['Why did the atom cross the road?', 'To get to the other set().'];
return lines[Math.floor(Math.random() * lines.length)];
}, { default: 'Click fetch' });

return () => html`
<div class="m-col m-gap">
<p class="muted">${joke()}</p>
<p class="muted m-t-5">${joke.status()}</p>
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-primary" @click=${() => joke.refetch()}
?disabled=${joke.isLoading()}>refetch</button>
<button @click=${() => joke.cancel()}>cancel</button>
</div>
</div>
`;
};
renderApp(App, document.getElementById('app'));