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 && {result.serverError}}
{result.data && 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 && (
<p className="error">{result.validationErrors.comment._errors[0]}</p>
)}
{hasSucceeded && <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!