safe-action-hooks

Use when executing next-safe-action actions from React client components -- useAction, useOptimisticAction, handling status/callbacks…

INSTALLATION
npx skills add https://github.com/next-safe-action/skills --skill safe-action-hooks
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

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 (

<form onSubmit={(e) => {

e.preventDefault();

const formData = new FormData(e.currentTarget);

execute({ name: formData.get("name") as string });

}}>

{isPending ? "Creating..." : "Create User"}

{result.serverError &#x26;&#x26; {result.serverError}}

{result.data &#x26;&#x26; Created: {result.data.id}}

);

}

## useOptimisticAction — Quick Start

"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 (

<label>

<input

type="checkbox"

checked={optimisticState.completed}

onChange={() => execute({ todoId: todo.id })}

/>

{todo.title}

</label>

);

}


## useStateAction — Quick Start

"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 (

<form action={formAction}>

<input name="rating" type="number" min="1" max="5" required />

<textarea name="comment" required />

<button type="submit" disabled={isPending}>

{isPending ? "Submitting..." : "Submit"}

</button>

{result.validationErrors?.comment &#x26;&#x26; (

<p className="error">{result.validationErrors.comment._errors[0]}</p>

)}

{hasSucceeded &#x26;&#x26; <p className="success">Thank you!</p>}

</form>

);

}


The server-side action must use `.stateAction()` (not `.action()`):

"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<Result>`
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 `<form action={formAction}>` pattern |

The hook return is itself a **discriminated union** keyed on `status` and every `has*` / `is*` shorthand (each typed as literal `true` / `false` per branch). Narrowing any discriminant narrows `result` — e.g. inside `if (hasSucceeded)`, `result.data` is `Data` (not `Data | undefined`). See [Type narrowing via hook status](https://github.com/next-safe-action/skills/blob/HEAD/skills/safe-action-hooks/./use-action.md#type-narrowing-via-hook-status).

## Supporting Docs

- [execute vs executeAsync, result handling](https://github.com/next-safe-action/skills/blob/HEAD/skills/safe-action-hooks/./use-action.md)

- [useStateAction in depth (decision table, formAction, initResult)](https://github.com/next-safe-action/skills/blob/HEAD/skills/safe-action-hooks/./use-state-action.md)

- [Optimistic updates with useOptimisticAction](https://github.com/next-safe-action/skills/blob/HEAD/skills/safe-action-hooks/./optimistic-updates.md)

- [Status lifecycle and all callbacks](https://github.com/next-safe-action/skills/blob/HEAD/skills/safe-action-hooks/./status-callbacks.md)

- [throwOnNavigation flag](https://github.com/next-safe-action/skills/blob/HEAD/skills/safe-action-hooks/./throw-on-navigation.md)

## Anti-Patterns

// BAD: Using executeAsync without try/catch when navigation errors are possible

const handleClick = async () => {

const result = await executeAsync({ id }); // Throws on redirect!

showToast(result.data);

};

// GOOD: Wrap executeAsync in try/catch

const handleClick = async () => {

try {

const result = await executeAsync({ id });

showToast(result.data);

} catch (e) {

// Handle non-navigation errors here if needed, then re-throw

// Navigation errors must propagate to Next.js

throw e;

}

};

// BAD: Using .action() with useStateAction — type error

const myAction = actionClient.inputSchema(schema).action(async ({ parsedInput }) => { ... });

useStateAction(myAction); // TypeScript error!

// GOOD: Use .stateAction() for useStateAction

const myAction = actionClient.inputSchema(schema).stateAction(async ({ parsedInput }, { prevResult }) => { ... });

useStateAction(myAction); // Works!

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card