Behaviour

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 pressButton("Save", { onClick: save })
Typing / value changesInput("q", { onChange: (v) => $q = v })
A form submitForm("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.

Live
$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.

Live
$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:

Live
$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:

Live
$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" })]))

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:

ToolTriggered byUse for
Actionfunction 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 changingPure, derived reactive values. No side effects.

Next