---
title: ".action()"
description: "Define named synchronous or asynchronous island operations with reactive pending, data, and error state."
---

> Documentation Index
> Fetch the complete documentation index at: https://ilha.build/llms.txt
> Use this file to discover all available pages before exploring further.

# .action()

Defines a named operation that belongs to one mounted island. Use actions for form submissions, mutations, and other work that needs reactive pending, result, and error state.

When a shorthand island needs a reusable operation, expand `ilha(() => JSX)` into `.action(...).render(...)`. The builder adds the action without changing the island model.

[Interactive Tutorial](/tutorial/counter/action)

## Basic usage

```tsx twoslash
import ilha from "ilha";

const Counter = ilha
  .state("count", 0)
  .action("increment", (amount: number, { state }) => {
state.count((count) => count + amount);
return state.count();
  })
  .render(({ state, action }) => (
<button onclick={() => action.increment(1)}>
  Count: {state.count()}
</button>
  ));
```

The first callback argument is the action payload. The second is the action context.

## Call actions from events

Each action becomes a function on the render context's `action` object:

```ts
action.increment(1);
```

An action without a payload is callable with no arguments:

```tsx twoslash
import ilha from "ilha";

const Reset = ilha
  .state("count", 1)
  .action("reset", (_, { state }) => state.count(0))
  .render(({ action }) => (
<button onclick={action.reset}>Reset</button>
  ));
```

Use an action directly as a native event handler when its payload matches the event type:

```tsx twoslash
import ilha from "ilha";

const Form = ilha
  .action("submit", async (event: SubmitEvent, { signal }) => {
event.preventDefault();
const form = event.currentTarget as HTMLFormElement;
const response = await fetch("/api/profile", {
  method: "POST",
  body: new FormData(form),
  signal,
});
if (!response.ok) throw new Error("Save failed");
return response.json();
  })
  .render(({ action }) => (
<form onsubmit={action.submit}>
  <input name="name" />
  <button disabled={action.submit.pending}>
    {action.submit.pending ? "Saving…" : "Save"}
  </button>
</form>
  ));
```

### Transform event data

Keep DOM-specific conversion in the native handler when an action accepts an application payload:

```tsx twoslash
import ilha from "ilha";

const Newsletter = ilha
  .action("subscribe", async (email: string, { signal }) => {
await fetch("/api/newsletter", {
  method: "POST",
  body: JSON.stringify({ email }),
  signal,
});
  })
  .render(({ action }) => (
<form
  onsubmit={(event) => {
    event.preventDefault();
    const email = String(
      new FormData(event.currentTarget).get("email") ?? "",
    );
    action.subscribe(email);
  }}
>
  <input name="email" type="email" required />
  <button disabled={action.subscribe.pending}>
    {action.subscribe.pending ? "Joining…" : "Join"}
  </button>
</form>
  ));
```

The native handler owns event behavior and payload conversion. The action owns the operation and its reactive state.

### Reuse an action from multiple handlers

Call the same action from any rendered element:

```tsx twoslash
import ilha from "ilha";

const Quantity = ilha
  .state("count", 1)
  .action("change", (amount: number, { state }) => {
state.count((count) => Math.max(0, count + amount));
  })
  .render(({ state, action }) => (
<div>
  <button onclick={() => action.change(-1)}>Remove</button>
  <output>{state.count()}</output>
  <button onclick={() => action.change(1)}>Add</button>
</div>
  ));
```

## Use `.on()` for advanced listeners

Prefer native JSX event props when one rendered element owns the event. Use `.on()` when you need a CSS selector, an island-host listener, the full island handler context, or multiple listener modifiers.

The first argument combines an optional selector and event name with `@`:

```ts
.on("button@click", handler) // descendant buttons
.on("@click", handler) // island host
.on("button@click:once:capture", handler) // combined modifiers
```

Declare actions before `.on()` to expose them through the handler context:

```tsx twoslash
import ilha from "ilha";

const Registration = ilha
  .action(
"registerUser",
async (event: SubmitEvent, { signal }) => {
  event.preventDefault();
  const form = event.currentTarget as HTMLFormElement;
  await fetch("/api/register", {
    method: "POST",
    body: new FormData(form),
    signal,
  });
},
  )
  .on("form@submit", ({ action, event }) =>
action.registerUser(event),
  )
  .render(() => (
<form>
  <input name="email" type="email" />
  <button>Register</button>
</form>
  ));
```

The `.on()` handler receives `state`, `derived`, `action`, `input`, `host`, `target`, `event`, and `signal`. Synchronous signal writes are batched, listeners clean up on unmount, and thrown errors route through [`.onError()`](/guide/island/onerror).

## Reactive action state

Every action exposes three read-only reactive properties:

```ts
action.save.pending; // boolean
action.save.data; // returned value | undefined
action.save.error; // Error | undefined
```

Reading these properties inside `.render()` or `.effect()` subscribes that scope. Ilha re-renders when the action state changes.

When asynchronous invocations overlap, `pending` stays `true` until all of them settle. Only the latest-started invocation can publish `data` or `error`, so an older response cannot replace a newer result.

Starting another invocation keeps the previous successful data available while pending. A successful invocation clears the previous error.

## Action context

The callback receives an `ActionContext` as its second argument:

```ts
{
  state: IslandState; // local state accessors
  derived: IslandDerived; // current derived values
  input: TInput; // resolved input props
  host: Element; // mounted island host
  signal: AbortSignal; // aborts when the island unmounts
}
```

Pass `signal` to `fetch` and other abort-aware APIs. Ilha ignores action settlements after unmount.

Actions are also available as `action` inside `.on()`, `.effect()`, `.onMount()`, and `.onError()` callbacks when you declare the action earlier in the builder chain.

## Errors

Ilha stores a thrown or rejected error in `action.name.error`. It also routes the error through the island error sink with `source: "action"`:

1. Local [`.onError()`](/guide/island/onerror) handlers
2. Global `onUncaughtError()` handlers
3. `console.error`

An `AbortError` caused by unmount cancellation is expected and does not reach the error sink.

```tsx twoslash
import ilha from "ilha";

const Save = ilha
  .action("save", async () => {
throw new Error("Could not save");
  })
  .onError(({ error, source }) => {
if (source === "action") console.error(error.message);
  })
  .render(({ action }) => (
<>
  <button onclick={action.save}>Save</button>
  {action.save.error && <p>{action.save.error.message}</p>}
</>
  ));
```

## SSR and hydration

Actions are client operations. During SSR, every action reports `pending === false`, with no data or error. Calling an action during SSR does nothing and logs a development warning.

Action state is transient and is not included in hydration snapshots. A hydrated island starts with idle action state.

## Notes

- Action keys must be unique within one builder chain.
- The action function returns `void`; read its result through `data` instead of awaiting the call site.
- Synchronous state writes inside an action are batched into one propagation pass.
- Use [`@ilha/store` actions](/guide/libraries/store) for mutations shared across multiple islands.

Source: https://ilha.build/guide/island/action/index.mdx
