Hooks
The useAction hook is the primary way to execute safe actions from Client Components. It provides reactive status tracking, lifecycle callbacks, and loading states.
Basic usage
"use client";
import { useAction } from "next-safe-action/hooks";
import { myAction } from "./actions";
export default function MyComponent() {
const { execute, result, status, isExecuting } = useAction(myAction);
return (
<div>
<button onClick={() => execute({ name: "Alice" })} disabled={isExecuting}>
{isExecuting ? "Loading..." : "Run action"}
</button>
{result.data && <p>Success: {JSON.stringify(result.data)}</p>}
{result.serverError && <p>Error: {result.serverError}</p>}
</div>
);
}Return object
useAction returns an object with these properties:
| Property | Type | Description |
|---|---|---|
execute | (input) => void | Execute the action (fire-and-forget) |
executeAsync | (input) => Promise<Result> | Execute and await the result |
result | SafeActionResult | The latest action result |
input | Input | undefined | The current/last input passed to execute |
status | HookActionStatus | Current status string |
reset | () => void | Reset to initial state |
isIdle | boolean | true when no action has been executed |
isExecuting | boolean | true while the action is running |
isTransitioning | boolean | true during React transition |
isPending | boolean | true when executing or transitioning |
hasSucceeded | boolean | true after a successful execution |
hasErrored | boolean | true after a failed execution |
hasNavigated | boolean | true after a framework navigation (redirect, etc.) |
Status lifecycle
The status property transitions through these states:
The boolean shortcuts (isExecuting, hasSucceeded, etc.) are derived from status for convenience.
Type narrowing
The hook return object is a discriminated union keyed on status and the shorthand booleans (hasSucceeded, hasErrored, etc.). Checking any discriminant narrows the result type:
const action = useAction(myAction);
// Narrowing via status
if (action.status === "hasSucceeded") {
action.result.data; // Data (guaranteed present)
action.result.serverError; // undefined (narrowed away)
action.result.validationErrors; // undefined (narrowed away)
}
// Narrowing via shorthand booleans
if (action.hasErrored) {
action.result.data; // undefined (narrowed away)
// Further narrow between error kinds:
if (action.result.serverError) {
action.result.validationErrors; // undefined
}
}Destructured narrowing works too (TypeScript 4.6+):
const { status, result, hasSucceeded } = useAction(myAction);
if (status === "hasSucceeded") {
result.data; // narrowed to Data
}
if (hasSucceeded) {
result.data; // also narrowed to Data
}This applies to all hooks: useAction, useOptimisticAction, and useStateAction. The result field itself is also a discriminated union (see Action result), so you get two layers of narrowing: status-level and result-level.
execute vs executeAsync
| Method | Returns | Use when |
|---|---|---|
execute(input) | void | You want fire-and-forget, handling results via callbacks or the reactive result property |
executeAsync(input) | Promise<Result> | You need to await the result inline (e.g., sequential calls, conditional logic) |
// Fire-and-forget, result is available reactively
execute({ name: "Alice" });
// Await the result: useful for sequential operations
const result = await executeAsync({ name: "Alice" });
if (result.data) {
await executeAsync({ name: "Bob" });
}executeAsync throws the server error if the action fails with a server error, so wrap it in a try/catch if needed. execute never throws, errors are always captured in result.
Options and callbacks
Pass options and callbacks as the second argument to useAction:
const { execute } = useAction(myAction, {
onExecute: ({ input }) => {
// Fires immediately when execute() is called
console.log("Starting with input:", input);
},
onSuccess: ({ data, input }) => {
// Fires when the action succeeds
toast.success(`Created: ${data.name}`);
},
onError: ({ error, input }) => {
// Fires when the action fails (validation or server error)
if (error.validationErrors) {
toast.error("Invalid input");
} else if (error.serverError) {
toast.error(error.serverError);
}
},
onNavigation: ({ navigationKind }) => {
// Fires when the action triggers a framework navigation
// (redirect, notFound, forbidden, unauthorized)
console.log("Navigating:", navigationKind);
},
onSettled: ({ result, input }) => {
// Fires after every execution (success, error, or navigation)
// Like finally in a try/catch
analytics.track("action_completed");
},
});throwOnNavigation
By default, navigation errors (notFound(), forbidden(), unauthorized(), redirect()) are caught by the hook and set the status to "hasNavigated", with onNavigation and onSettled callbacks firing normally.
Set throwOnNavigation: true to propagate navigation errors to the nearest error boundary instead. In Next.js, this shows the appropriate error page (404, 403, 401):
const { execute } = useAction(myAction, {
throwOnNavigation: true,
// onNavigation and onSettled are NOT available here (TypeScript enforced)
});When throwOnNavigation is true, onNavigation and onSettled are not available because React's rendering model prevents effects from running when a component throws during render. For guaranteed navigation side effects, use server-side action callbacks.
Callback execution order
onExecute: always first- One of:
onSuccess,onError, oronNavigation onSettled: always last
useStateAction
Use useStateAction for stateful actions (defined with .stateAction()) that need full lifecycle control. It provides everything useAction offers, plus formAction for <form action={formAction}> integration and automatic prevResult management.
Basic usage
"use client";
import { useStateAction } from "next-safe-action/hooks";
import { myStatefulAction } from "./actions";
export default function MyForm() {
const { formAction, result, status, isPending, hasSucceeded, reset } = useStateAction(myStatefulAction, {
onSuccess: ({ data }) => {
console.log("Success:", data);
},
onError: ({ error }) => {
console.error("Error:", error.serverError);
},
});
return (
<form action={formAction}>
<input name="email" type="email" placeholder="Email" />
{result.validationErrors?.email && <p>{result.validationErrors.email._errors[0]}</p>}
<button type="submit" disabled={isPending}>
{isPending ? "Submitting..." : "Submit"}
</button>
{hasSucceeded && <p>Success!</p>}
</form>
);
}Return object
Same as useAction plus:
| Property | Type | Description |
|---|---|---|
formAction | (input) => void | Dispatcher for <form action={formAction}> pattern |
useAction vs useStateAction
useAction | useStateAction | |
|---|---|---|
| Action method | .action() | .stateAction() |
| Previous result | Not available | Server code receives prevResult |
| Form action | Not supported | formAction for <form action={...}> |
| Triggers | execute(input) (programmatic) | execute(input), formAction, or executeAsync(input) |
| Best for | Interactive UI, buttons, events | Forms with state, multi-step wizards |
Use useAction when you don't need previous result access and triggers are programmatic. Use useStateAction when you need prevResult in server code, want <form action={formAction}>, or are building multi-step forms.
useStateAction does not support no-JS progressive enhancement. The hook wraps the action to enable error tracking and callbacks, which requires JavaScript. For forms that must work without JavaScript, use React's useActionState directly. See the form actions guide for a comparison.