---
name: safe-action-hooks
description: Use when executing next-safe-action actions from React client components -- useAction, useOptimisticAction, handling status/callbacks (onSuccess/onError/onSettled), execute vs executeAsync, or optimistic UI updates
---
# next-safe-action React Hooks
## Import
```ts
// All hooks
import { useAction, useOptimisticAction, useStateAction } from "next-safe-action/hooks";
// Backward-compatible re-export (same useStateAction hook)
import { useStateAction } from "next-safe-action/stateful-hooks";
```
## useAction — Quick Start
```tsx
"use client";
import { useAction } from "next-safe-action/hooks";
import { createUser } from "@/app/actions";
export function CreateUserForm() {
const { execute, result, status, isExecuting, isPending } = useAction(createUser, {
onSuccess: ({ data }) => {
console.log("User created:", data);
},
onError: ({ error }) => {
console.error("Failed:", error.serverError);
},
});
return (
);
}
```
## useOptimisticAction — Quick Start
```tsx
"use client";
import { useOptimisticAction } from "next-safe-action/hooks";
import { toggleTodo } from "@/app/actions";
export function TodoItem({ todo }: { todo: Todo }) {
const { execute, optimisticState } = useOptimisticAction(toggleTodo, {
currentState: todo,
updateFn: (state, input) => ({
...state,
completed: !state.completed,
}),
});
return (
);
}
```
## useStateAction — Quick Start
```tsx
"use client";
import { useStateAction } from "next-safe-action/hooks";
import { submitFeedback } from "@/app/actions";
export function FeedbackForm() {
const { formAction, result, isPending, hasSucceeded } = useStateAction(submitFeedback, {
onSuccess: ({ data }) => {
console.log("Submitted:", data);
},
onError: ({ error }) => {
console.error("Failed:", error.serverError);
},
});
return (
);
}
```
The server-side action must use `.stateAction()` (not `.action()`):
```ts
"use server";
import { z } from "zod";
import { actionClient } from "@/lib/safe-action";
export const submitFeedback = actionClient
.inputSchema(z.object({ rating: z.number().min(1).max(5), comment: z.string() }))
.stateAction(async ({ parsedInput }, { prevResult }) => {
// prevResult contains the previous SafeActionResult
await db.feedback.create({ data: parsedInput });
return { rating: parsedInput.rating };
});
```
## Return Value
All hooks (`useAction`, `useOptimisticAction`, `useStateAction`) return:
| Property | Type | Description |
|---|---|---|
| `execute(input)` | `(input) => void` | Fire-and-forget execution |
| `executeAsync(input)` | `(input) => Promise` | Returns a promise with the result |
| `input` | `Input \| undefined` | Last input passed to execute |
| `result` | `SafeActionResult` | Last action result — **discriminated union** of 4 branches (idle / success / serverError / validationErrors); narrowed when you check `status` or any `has*` shorthand |
| `reset()` | `() => void` | Resets all state to initial values |
| `status` | `HookActionStatus` | Current status string |
| `isIdle` | `boolean` | No execution has started yet |
| `isExecuting` | `boolean` | Action promise is pending |
| `isTransitioning` | `boolean` | React transition is pending |
| `isPending` | `boolean` | `isExecuting \|\| isTransitioning` |
| `hasSucceeded` | `boolean` | Last execution returned data |
| `hasErrored` | `boolean` | Last execution had an error |
| `hasNavigated` | `boolean` | Last execution triggered a navigation |
`useOptimisticAction` additionally returns:
| `optimisticState` | `State` | The optimistically-updated state |
`useStateAction` additionally returns:
| `formAction` | `(input) => void` | Dispatcher for `