Lifecycle & Actions

action

The primitive for mutations: imperative async workflows whose writes span an async gap — optimistic write, server round-trip, reconciling write — where intermediate state must not leak and failure must revert cleanly (pair with createOptimistic / createOptimisticStore).

Navigation-shaped updates do not need an action. A plain setter call is enough: reads pull the async, and downstream async computeds hold their previous values per-node until the new ones are ready (isPending / latest expose the in-flight state). Reach for action only when writes happen after async work, not merely upstream of it.

Framework-level actions (router form actions, server actions) are specializations of this primitive: they are actions in exactly this sense — the same transactional semantics — with form binding, serialization, and submission tracking layered on top. The shared name is deliberate.

Wraps a generator function so each invocation runs as a single transaction that holds every signal/store write between yields. The surrounding UI sees one atomic update when the action completes; nothing is committed until every action in the transaction has returned.

yield is the transaction-safe suspension point: the action waits for a yielded promise and re-enters the transaction before running the code after it. A plain await does not — the runtime has no hook into an async generator's internal await continuations, so code between an await and the next yield runs OUTSIDE the transaction: writes to fresh signals commit immediately, and anything that creates a reader there — until(), latest(), a memo or effect, a mount — is created mainline, where a read of this action's held state makes it born held (A29): staged with the transaction and replayed at its commit. await is still the ergonomic choice for typed results; just put a bare yield before any write or reader creation that follows it — including the expression of the next yield, which is evaluated before the step re-enters:

const saved = await api.createTodo(text); // typed result
yield; // re-enter the transaction before writing or reading
setTodos(t => { ... });
yield until(() => todos.some(t => t.id === saved.id));

(For the same reason, don't call flush() inside an action body — it cannot reveal the held writes and is refused.)

Each call returns a Promise that resolves with the generator's return value, or rejects if it throws. Pair with createOptimistic / createOptimisticStore to apply tentative writes that auto-revert if the action fails.


Import

import { action } from "solid-js";

Type signature

function action<Args extends any[], Y, R>(
genFn: (...args: Args) => Generator<Y, R, any> | AsyncGenerator<Y, R, any>
): (...args: Args) => Promise<R>;

Parameters

genFn

  • Type: (...args: Args) => Generator<Y, R, any> | AsyncGenerator<Y, R, any>

Generator or async generator. Each yield is a suspension point that re-enters the transaction; writes between yields commit together.


Return value

A function with the generator's parameters that returns a promise for its return value. The promise rejects when the generator throws.


Examples

const [todos, setTodos] = createOptimisticStore<Todo[]>([]);
const addTodo = action(async function* (text: string) {
const tempId = crypto.randomUUID();
setTodos((t) => {
t.push({ id: tempId, text, pending: true });
}); // optimistic
const saved = await api.createTodo(text); // network round-trip, typed
yield; // re-enter the transaction
setTodos((t) => {
const i = t.findIndex((x) => x.id === tempId);
if (i >= 0) t[i] = saved;
});
return saved;
});
await addTodo("buy milk");

Caveats

  • A plain await leaves the transaction. Put a bare yield before any writes that follow an await, or use yield in place of await.
  • Do not call flush() inside the generator; it drains the transaction mid-step.
  • Navigation-shaped updates (write an input, read the async result) do not need an action. A plain setter call is held automatically.
  • A failure rejects the returned promise and does not reach an Errored boundary. Catch it inside the generator, or with .catch() where the action is called.

Common problems


Learn more

Last updated: 10/22/26, 10:23 AMEdit this pageReport an issue with this page