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.

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

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.

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

  1. onExecute: always first
  2. One of: onSuccess, onError, or onNavigation
  3. 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:

PropertyTypeDescription
formAction(input) => voidDispatcher for <form action={formAction}> pattern

useAction vs useStateAction

useActionuseStateAction
Action method.action().stateAction()
Previous resultNot availableServer code receives prevResult
Form actionNot supportedformAction for <form action={...}>
Triggersexecute(input) (programmatic)execute(input), formAction, or executeAsync(input)
Best forInteractive UI, buttons, eventsForms 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.

What's next?

On this page