Overview
Actions replace the repetitive boilerplate of manually wiring isLoading, error, and data atoms around every async operation. They give you a centralized, reusable execution point that is reactive, subscribable, and testable.
Choose the primitive that matches your use case: action for synchronous logic with decoupled subscribers, asyncAction for async state machines, fetchAction/getAction for HTTP, paginatedAsyncAction for paginated data, and taskAction for queued workflows.
Prefer asyncAtom when a resource should auto-refetch from atom keys and be read like an atom. Prefer asyncAction / fetch helpers when call sites pass arguments.
Action types at a glance
|
Type
|
Best for
|
Built-in reactive atoms?
|
|---|---|---|
action
| Sync |
Synchronous domain operations. Use .__subscribe() for side-effects — no loading atoms.
|
asyncAction
| Async |
Any Promise workflow. Gives .data(), .isLoading(), .error(), .status().
|
fetchAction family
| HTTP | asyncAction + Fetch URL/method config. See fetch-actions. |
paginatedAsyncAction
| Async + pages |
Adds .page(), .totalPages(), and .next() on top of asyncAction.
|
taskAction
| Queued | Queue-oriented progress with concurrency and per-task status. |
asyncAtom
| Resource | Keyed/manual fetch read like an atom — not an action. See State. |
action — synchronous, trackable operations
action wraps a synchronous function and adds
observability: subscribers, before/after interceptors, and a
consistent callable interface. The result is still returned
synchronously on every call.
Reference
import { action } from 'mates';
const addToCart = action((item: string, qty = 1) => {
cart[item] = (cart[item] ?? 0) + qty;
return { item, qty, total: cart[item] };
});
// Call like a normal function — result returned synchronously
addToCart('widget', 2);
// Subscribe to every call's result (__subscribe, not subscribe)
const unsub = addToCart.__subscribe((result) => {
console.log('Added to cart:', result);
});
// Intercept before execution (validation, transformation)
addToCart.interceptBefore((next, item, qty) => {
if (!item) throw new Error('Item is required');
return next(item.trim(), qty);
});
// Intercept after execution (enrich the result)
addToCart.interceptAfter((result) => ({ ...result, at: Date.now() }));
// Unsubscribe when done
unsub();
paginatedAsyncAction — paginated data fetching
paginatedAsyncAction extends
asyncAction with a reactive page atom,
totalPages, and next(). Each call fetches
the current page — it does not accumulate previous pages.
import { paginatedAsyncAction } from 'mates';
const listUsers = paginatedAsyncAction(async () => {
const page = listUsers.page.val;
const res = await fetch(
`https://jsonplaceholder.typicode.com/users?_page=${page}&_limit=5`,
{ signal: listUsers.abortController.signal },
);
const total = Number(res.headers.get('X-Total-Count') || 10);
listUsers.totalPages.set(Math.ceil(total / 5));
return res.json();
});
listUsers();
listUsers.next();
listUsers.page.set(1);
listUsers();
taskAction — queued concurrent workflows
taskAction queues invocations and runs them with
configurable concurrency. Ideal for file uploads, background jobs, or
any workflow where per-task progress and cancellation matter.
import { taskAction } from 'mates';
const uploadFile = taskAction(async (file: File, signal: AbortSignal) => {
const form = new FormData();
form.append('file', file);
const res = await fetch('/api/upload', { method: 'POST', body: form, signal });
return res.json();
});
uploadFile.clearAndStart([file1, file2, file3], 2);
uploadFile.isRunning();
uploadFile.progress();
uploadFile.tasks();
uploadFile.successTasks();
uploadFile.stop();
action — live demo
interceptBefore / interceptAfter run on every call; __subscribe logs the enriched result — no hand-rolled event bus.