next-safe-action
Guides

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:

PropertyTypeDescription
execute(input) => voidExecute the action (fire-and-forget)
executeAsync(input) => Promise<Result>Execute and await the result
resultSafeActionResultThe latest action result
inputInput | undefinedThe current/last input passed to execute
statusHookActionStatusCurrent status string
reset() => voidReset to initial state
isIdlebooleantrue when no action has been executed
isExecutingbooleantrue while the action is running
isTransitioningbooleantrue during React transition
isPendingbooleantrue when executing or transitioning
hasSucceededbooleantrue after a successful execution
hasErroredbooleantrue after a failed execution
hasNavigatedbooleantrue after a framework navigation (redirect, etc.)

Status lifecycle

The status property transitions through these states:

Hook status lifecycle: idle → executing → hasSucceeded / hasErrored / hasNavigated

The boolean shortcuts (isExecuting, hasSucceeded, etc.) are derived from status for convenience.

execute vs executeAsync

MethodReturnsUse when
execute(input)voidYou 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.

Callbacks

Pass callbacks as the second argument to useAction for lifecycle events:

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");
	},
});

Callback execution order

  1. onExecute — always first
  2. One of: onSuccess, onError, or onNavigation
  3. onSettled — always last

What's next?

On this page