Summary#
A component's action and its plain method are the SAME imperative-body mechanism wearing two headers. Both
read and write the component's own state, both write entities and commit, both invoke effects, both bind to an
event prop (onClick/onChange/onSubmit), and both expose a .Pending flag a control can read while an
invocation of them is in flight. Nothing is reachable from an action that a method cannot also do. The one
structural difference is that an action is always void — it cannot declare a return type — while a method may,
and a return type is what lets a method be called for its value from a render expression
(Calling helpers from render).
Signature#
action name(params) { … } // always void — no return type may be written
RetType name(params) => expr; // a plain method — may return a value
RetType name(params) { … } // …or a block body; `void` is legal tooDescription#
What's actually different — and what isn't#
action | method | |
|---|---|---|
| Body grammar | statements, same as a method | statements, or a single expression body |
| May read/write the component's own fields | yes | yes |
May write entities and UnitOfWork.Commit() | yes | yes |
May invoke effects (Log, Http, a server function, …) | yes | yes |
Bindable to an event prop (onPress: Save) | yes | yes — the return value, if any, is simply discarded |
Callable from another imperative body (on mount { Reload(); }) | yes | yes |
Reading .Pending on the member | yes (AL58) | yes — identical mechanism (Pending) |
| Declares a return type | no — always void | yes |
Callable for its value from a render expression, if pure | not useful — has no value to give | yes (Calling helpers from render) |
async belongs to neither: it is refused anywhere in Osy#, on a function, a class method, or a component member
alike — effects run in place, so there is nothing for it to mean.
Everything in the first seven rows is the SAME mechanism, checked the SAME way, regardless of which keyword declared the member. The only row that is actually structural is "declares a return type" — and the render-call row follows from it, not from a separate rule about actions.
Nothing is only possible from an action#
A plain void method writes an entity, commits, and binds directly to an event prop — no action keyword
anywhere:
```osy title="a plain method writes, commits, and binds to an event — no action needed" test app=ui-action
entity Widget {
[Required, MaxLength(80)] string Name;
security { allow read, create when IsAuthenticated || IsAnonymous; }
}
[Route("/widgets")] [AllowAnonymous] [Render(CSR)] component WidgetsPage() { live var widgets = Widget.ToList();
void AddOne() { var w = new Widget { Name = "from a method" }; UnitOfWork.Commit(); }
render { Stack { foreach (var w in widgets) { Text(w.Name); } Button("Add", onPress: AddOne); } } }
### `.Pending` reads the same on both {#pending}
`<verb>.Pending` ([Pending](/reference/ui/pending/)) is not an `action`-only surface — it is typed off a real baseline class
(`Osysharp.ActionState`) that every declared VERB gets a read on, action or method alike:
```osy title="`.Pending` on a plain method — identical to an action's" test app=ui-action
[Route("/reload")]
[AllowAnonymous]
[Render(CSR)]
component ReloadRow() {
live var widgets = Widget.ToList();
int Reload() { return widgets.Count; }
render {
Stack {
Text(Reload.Pending ? "Counting…" : "Idle");
Button("Recount", onPress: Reload);
}
}
}There is no .Error on a verb of either kind — ActionState carries exactly one field, Pending. save.Error
is refused the same as any other unknown member; the unrelated .error that a server-read QUERY exposes
(var x = Entity.ToList() → .items/.total/.loading/.error/.hasMore, see component) is a different
surface at a different grain.
Only a method is USEFUL to call from render — and the check does not care which keyword you wrote#
Calling helpers from render lets a render expression call a pure client helper and render the value it returns. A method can be that helper because it may declare a real return type; an action cannot, because it is always void and has nothing to give render. This is a consequence of the return-type rule, not a second, action-shaped restriction — the purity check itself inspects what a body DOES, never which keyword declared it:
action Bump() { count = count + 1; }
render { Text(Bump()); }
// error: `Bump(…)` cannot be called from a render expression — it assigns the component's own state (`count`).
// A render expression must run synchronously and purely on the client; move the call into an `action`/`on mount`
// and render the value it produces.A method with the identical body (int BumpValue() { count = count + 1; return count; }) is refused with the
identical message, naming BumpValue instead of Bump — the check never asked which keyword declared the member.
When to reach for which#
- Something a control binds to —
onClick/onChange/onSubmit, or a lifecycle hook that just performs work — is anaction. It reads as an event name, and it is the shape every existing example in this reference uses for a handler. - Something that computes and returns a value — a subtotal, a formatted label, a helper two actions both
call and both need the result of — is a
method. It is callable from render when pure, and its name reads as a question with an answer rather than as an event. - Logic several actions share that does not need the component's own state at all belongs in a class instead of either — see [[class-methods#method-or-function]] for the split, and store for the (currently limited) story on sharing state across components.
See also#
- component — the member table this page's "difference" row expands on, and the receiver model both kinds share.
- Pending — the full
.Pending/Pending.Anyreference. - Calling helpers from render — the purity rule that decides what may be called from render, and why only a
methodis useful there. - class methods — moving shared logic out of a component entirely.
- store — sharing STATE and actions across components (what works today, and what does not yet).