Actions.
Actions are function declarations that run when
the user does something — clicks a button, types in a
field, submits a form. They are the imperative counterpart to reactive
bindings: where a binding describes a value, an action describes what
happens next.
Anatomy
An action is an ordinary function declaration: function name(args) { … }.
Every function declaration creates both a component and an
action with the same name — if it returns a value, that value is
rendered when called in a render position; if it runs for side effects,
the same name can be referenced from an event handler. The first-letter
case of the name is not significant: save and
Save are equally valid.
| Wire it to… | Prop |
|---|---|
| A button press | Button("Save", { onClick: save }) |
| Typing / value changes | Input("q", { onChange: (v) => $q = v }) |
| A form submit | Form("login", { onSubmit: submit, fields: […] }) |
When the event fires the runtime runs the function, applies every state
write atomically, then re-renders once. Pass the name
(onClick: save) when the handler takes no arguments, or a thin
lambda (onClick: () => remove(id)) when you need to forward
something.
Reading & writing state
The only place a script mutates state is inside an action (or an effect /
lambda body). Writing $name = value is reactive — every
binding that reads $name re-renders. The compound assignment
operators += -= *= /= ??= and ++ -- all work, and
member writes like $user.name = "Alex" rebuild the root object
immutably so subscribers see a fresh reference.
$count = 0
function inc() { $count = $count + 1 }
function dec() { $count -= 1 }
$app(Card([Column([
Text(`Count: ${$count}`, { variant: "large-heavy" }),
Row([
Button("−", { onClick: dec, variant: "ghost" }),
Button("Increment", { onClick: inc, variant: "primary" })
], { gap: "sm" })
], { gap: "md" })]))
Event handlers: inline vs named
For a one-liner, an inline lambda right on the prop is the cleanest thing:
onClick: () => $open = true. Reach for a named
function when the body grows past a statement or two, when you
want to reuse it across several controls, or when it simply reads better
with a name.
$open = false
function close() { $open = false }
$app(Column([
Button("Open", { onClick: () => $open = true, variant: "primary" }),
$open ? Callout("info", { title: "Now open", description: "Inline lambda opened me; a named action closes me." }) : Text("Closed."),
$open ? Button("Close", { onClick: close, variant: "ghost" }) : Spacer()
], { gap: "md" }))
Passing arguments
When a handler needs to know which item it is acting on, wrap the
call in a lambda that forwards the id. This is the standard shape for
per-row buttons built with .map:
$items = [{ id: 1, name: "Apples" }, { id: 2, name: "Pears" }, { id: 3, name: "Plums" }]
function remove(id) { $items = $items.filter(x => x.id != id) }
$app(Card([Column($items.map(item => Row([
Text(item.name),
Spacer(),
Button("Remove", { onClick: () => remove(item.id), variant: "ghost", size: "sm" })
], { gap: "sm" })), { gap: "sm" })]))
Talking to the server
A write to the server is just an $http({...}) call with a
non-GET method, fired from inside an action. $http(...) returns
a reactive resource — bind it to an atom and you get
.data, .error, .loading,
.refetch(), and .cancel().
The clean way to refresh a list after a write is the settable
.onDone callback. Assign it after creating the resource; it
fires once every time the request settles — on the initial
load and every refetch(), on both success and error. It does
not fire for a request that was superseded or cancel()led.
$todos = $http({ url: "https://api.example.com/todos" })
function save() {
$res = $http({
url: "https://api.example.com/todos",
method: "POST",
body: { title: $draft, done: false }
})
$res.onDone = () => { $todos.refetch() } // re-pull the list once the write settles
$draft = ""
}
addBtn = Button("Add", { onClick: save, variant: "primary" })
Render the resource's states with Async($res, { loading, error, data }),
and see the Http guide for the full resource API.
For create/update/delete that should fire on a click (not on mount), prefer
$mutation({ url, method }) — a deferred write you trigger with
.mutate(overrides?). It also supports optimistic
updates (apply state instantly, auto‑rollback on failure) and
cache invalidation. See
Http → Mutations.
$addTodo = $mutation({
url: "https://api.example.com/todos",
optimistic: o => { $todos = [...$todos, o.body] },
invalidates: ["todos"]
})
Button("Add", { onClick: () => $addTodo.mutate({ body: { title: $draft } }) })
Confirming destructive actions
The full JavaScript global surface is available inside an action body, so
the browser dialogs — confirm(...), alert(...),
prompt(...) — work directly. Guard a destructive write
with confirm and bail early when the user cancels:
$items = [{ id: 1, name: "Draft.txt" }, { id: 2, name: "Notes.md" }, { id: 3, name: "Plan.pdf" }]
function del(id) {
if (!confirm("Delete this item?")) { return }
$items = $items.filter(x => x.id != id)
}
$app(Card([Column($items.map(f => Row([
Text(f.name),
Spacer(),
Button("Delete", { onClick: () => del(f.id), variant: "danger", size: "sm" })
], { gap: "sm" })), { gap: "sm" })]))
Navigating
route is the reactive router handle — never declare it
yourself. Call route.navigate("/path") from inside an action to
change the URL and transition the matched view:
function openOrder(id) {
route.navigate("/orders/" + id)
}
row = Button("View", { onClick: () => openOrder(order.id), variant: "ghost" })
For matching, params, and query strings, see the Routing guide.
Emitting events to the host
Inside any action, $emit("name", detail) dispatches an outbound
CustomEvent on the <aktion-app> host element.
The page embedding the app listens with addEventListener. Pick
stable names; assistant-message, error, and
route-change are reserved.
function submit() {
$emit("form-submitted", { email: $email, name: $name })
}
// Host page:
el.addEventListener("form-submitted", (event) => {
console.log(event.detail.email);
});
Action vs effect vs binding
Three tools cover every "something should happen" case. Pick by what triggers it:
| Tool | Triggered by | Use for |
|---|---|---|
Action — function save() { … } | A user event (click, change, submit) | Mutations the user initiates: writes, server calls, navigation. |
Effect — $effect(() => { … }, [$dep]) | A dependency changing (or mount / interval) | Reactive side effects: debounced search, timers, subscriptions — see Side effects. |
Binding — $total = $util.sum($items) | Any read it depends on changing | Pure, derived reactive values. No side effects. |
Next
Side effects
$effect(...) for dependency-triggered work: timers, debounced search, cleanup.
JavaScript interactions
The full JS global surface, browser APIs, and host integration patterns inside action bodies.
Learn more → DataHttp
The reactive resource: .data .error .loading .refetch() .cancel() .onDone.