# useAction

## Import

```typescript
import { useAction } from "alepha/react";
```

## Overview

Hook for handling async actions with automatic error handling and event emission.

The handler takes an `ActionContext` FIRST, then the arguments `run()`
was given: `handler: async ({ signal }, id: string) => ...`.

`run()` never rejects. It resolves one `RunResult`:

- `{ ok: true, value }`: the handler returned `value`.
- `{ ok: false, reason: "error", error }`: the handler threw `error` (also
  in `error` state and emitted as `react:action:error`).
- `{ ok: false, reason: "dropped" }`: refused because a run was already in
  flight, or a debounced call replaced by a newer one.
- `{ ok: false, reason: "aborted" }`: cancelled by `cancel()`, an unmount
  or a newer run.

So a follow-up branches on the result: `if ((await save.run()).ok) close()`.

By default, prevents concurrent executions - if an action is running and you call it again,
the second call is dropped. Use `debounce` option to delay execution instead.

Emits lifecycle events:

- `react:action:begin` - When action starts
- `react:action:success` - When action completes successfully
- `react:action:error` - When action throws an error
- `react:action:end` - Always emitted at the end

## Examples

Basic usage

```tsx
const action = useAction(
  {
    handler: async (_ctx, data: Data) => {
      await api.save(data);
    },
  },
  [],
);

<button onClick={() => action.run(data)} disabled={action.loading}>
  Save
</button>;
```

With debounce (search input)

```tsx
const search = useAction(
  {
    handler: async (_ctx, query: string) => {
      await api.search(query);
    },
    debounce: 300, // Wait 300ms after last call
  },
  [],
);

<input onChange={(e) => search.run(e.target.value)} />;
```

Run on component mount

```tsx
const fetchData = useAction(
  {
    handler: async () => {
      const data = await api.getData();
      return data;
    },
    runOnInit: true, // Runs once when component mounts
  },
  [],
);
```

Run periodically (polling)

```tsx
const pollStatus = useAction(
  {
    handler: async () => {
      const status = await api.getStatus();
      return status;
    },
    runEvery: 5000, // Run every 5 seconds
  },
  [],
);

// Or with duration tuple
const pollStatus = useAction(
  {
    handler: async () => {
      const status = await api.getStatus();
      return status;
    },
    runEvery: [30, "seconds"], // Run every 30 seconds
  },
  [],
);
```

With AbortController

```tsx
const fetch = useAction(
  {
    handler: async ({ signal }, url: string) => {
      const response = await fetch(url, { signal });
      return response.json();
    },
  },
  [],
);
// Automatically cancelled on unmount or when new request starts
```

With error handling

```tsx
const deleteAction = useAction(
  {
    handler: async (_ctx, id: string) => {
      await api.delete(id);
    },
    onError: (error) => {
      if (error.code === "NOT_FOUND") {
        // Custom error handling
      }
    },
  },
  [],
);

{
  deleteAction.error && <div>Error: {deleteAction.error.message}</div>;
}
```

Global error handling

```tsx
// In your root app setup
alepha.events.on("react:action:error", ({ error }) => {
  toast.danger(error.message);
  Sentry.captureException(error);
});
```
