@zoijs/action
The write-side companion to resource. Wrap a submit, save, or delete and get reactive pending / error / done state.
npm install @zoijs/core @zoijs/actionExample#
import { html, mount } from "@zoijs/core";
import { action } from "@zoijs/action";
const save = action(async (formData) => {
const res = await fetch("/api/users", { method: "POST", body: formData });
if (!res.ok) throw new Error("Could not save");
return res.json();
});
function Form() {
return html`
<form onsubmit=${(e) => { e.preventDefault(); save.run(new FormData(e.currentTarget)); }}>
<input name="name" />
<button disabled=${() => save.pending()}>
${() => (save.pending() ? "Saving…" : "Save")}
</button>
${() => (save.error() ? html`<p role="alert">${save.error().message}</p>` : null)}
</form>
`;
}run() never throws — failures land in error(), so await save.run(...) is safe without try/catch.API#
| Member | What it does |
|---|---|
action(fn, { exclusive }?) | Wrap a write function; nothing runs until run() |
run(...args) | Call fn; sets pending, then done+result or error |
pending() | true while running |
error() | The error, or null |
done() | true after success |
result() | The latest successful result |
reset() | Clear all state |
One request at a time: exclusive#
For non-idempotent writes (a payment, a POST that creates a record), pass { exclusive: true }. While a run is pending, run() doesn't call fn again — it returns the pending run's promise — so a double-click can't send the request twice. The lock is taken before fn starts and released when the run succeeds or fails (or on reset()). Joined calls aren't errors and aren't reported to onError.
const pay = action((order) => fetch("/api/pay", { method: "POST", body: JSON.stringify(order) }), { exclusive: true });It doesn't stop server-side replays — keep the endpoint idempotent and protect it against CSRF (see Production Security).
Error monitoring#
A failure that becomes error() is also reported to the app's configure({ onError }) hook from @zoijs/core, as { kind: "action" } with the original error (superseded or post-unmount results aren't reported). It goes through the core's shared runtime, so a no-build import map needs no extra entry. Reports need @zoijs/core 1.9.0 or later — older cores have no shared runtime, so nothing is reported (and nothing breaks).
import { configure } from "@zoijs/core";
configure({ onError: (error, info) => reportToMonitoring(error, info.kind) });Known limitations#
- No cache, query invalidation, retries, or optimistic updates — by design.
- To refresh data after a write, call your resource's
refresh()yourself.