next-safe-action/skills

safe-action-hooks

Use when executing next-safe-action actions from React client components or building optimistic UI -- useAction, useOptimisticAction, useStateAction, useOptimisticStateAction, status/callbacks (onSuccess/onError/onSettled), execute vs executeAsync, formActi…

Ver código-fonte
Documento original do Skill

Renderizado do repositório de origem, preservando títulos, exemplos, código, tabelas, links e imagens.

next-safe-action React Hooks

Import

ts
// All hooks
import {
  useAction,
  useOptimisticAction,
  useStateAction,
  useOptimisticStateAction,
} from "next-safe-action/hooks";

// Backward-compatible re-export (same useStateAction hook)
import { useStateAction } from "next-safe-action/stateful-hooks";

Which Hook

HookAction methodUse for
useAction.action()Programmatic triggers, interactive UI, most cases
useOptimisticAction.action()Instant UI for changes that replace state (last-write-wins)
useStateAction.stateAction()<form action={formAction}>, prevResult on the server, queued dispatches
useOptimisticStateAction.stateAction()Instant UI for changes that accumulate: overlapping writes are queued, each folding over the confirmed state

Pick by action kind first: .action() pairs with useAction / useOptimisticAction, .stateAction() pairs with useStateAction / useOptimisticStateAction.

useAction — Quick Start

tsx
"use client";

import { useAction } from "next-safe-action/hooks";
import { createUser } from "@/app/actions";

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 });
    }}>
      <input name="name" required />
      <button type="submit" disabled={isPending}>
        {isPending ? "Creating..." : "Create User"}
      </button>
      {result.serverError && <p className="error">{result.serverError}</p>}
      {result.data && <p className="success">Created: {result.data.id}</p>}
    </form>
  );
}

useOptimisticAction — Quick Start

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

useOptimisticStateAction — Quick Start

For overlapping writes that must accumulate (reorder an item, then reorder it again before the first save lands). Dispatches are queued: each waits for the previous to settle, and the server receives the last confirmed state as prevResult.

tsx
"use client";

import { useOptimisticStateAction } from "next-safe-action/hooks";
import { moveItem } from "@/app/actions";
import { reorder } from "@/lib/reorder"; // the same reducer the server runs

export function ReorderList({ items }: { items: Item[] }) {
  const { execute, optimisticState, isPending } = useOptimisticStateAction(moveItem, {
    currentState: items, // must be a stable reference — compared by identity
    updateFn: reorder,
  });

  return (
    <ul data-saving={isPending}>
      {optimisticState.map((item) => (
        <li key={item.id}>
          {item.label}
          <button onClick={() => execute({ id: item.id, direction: "up" })}>Up</button>
        </li>
      ))}
    </ul>
  );
}

The action must be a .stateAction(), and must revalidate the state the page renders:

ts
"use server";

export const moveItem = actionClient
  .inputSchema(moveSchema)
  .stateAction(async ({ parsedInput }, { prevResult }) => {
    const next = reorder(prevResult.data!, parsedInput); // always the last confirmed state
    await db.items.save(next);
    revalidatePath("/items");
    return next;
  });

See useOptimisticStateAction in depth for the decision rules, the pending-changes-list shape, and the gotchas.

useStateAction — Quick Start

tsx
"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()). <form action={formAction}> submits raw FormData, so the input schema must parse FormData (e.g. with zod-form-data):

ts
"use server";

import { z } from "zod";
import { zfd } from "zod-form-data";
import { actionClient } from "@/lib/safe-action";

export const submitFeedback = actionClient
  .inputSchema(
    zfd.formData({
      rating: zfd.numeric(z.number().min(1).max(5)),
      comment: zfd.text(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, useOptimisticStateAction) return:

PropertyTypeDescription
execute(input)(input) => voidFire-and-forget execution
executeAsync(input)(input) => Promise<Result>Returns a promise with the result
input`Input \undefined`Last input dispatched (via execute, executeAsync, or formAction)
resultSafeActionResultLast action result — discriminated union of 4 branches (idle / success / serverError / validationErrors); narrowed when you check status or any has* shorthand
reset()() => voidResets client state to initial (restores initResult if provided) and ignores the result of any in-flight execution. It does not cancel the server call already in flight. On the queued hooks (useStateAction, useOptimisticStateAction) it also skips every dispatch still waiting its turn: their action never runs and no write happens
statusHookActionStatusCurrent status string
isIdlebooleanNo execution has started yet
isExecutingbooleanAction promise is pending
isTransitioningbooleanReact transition is pending
isPendingboolean`isExecuting \\isTransitioning, except after reset()`: a reset reports idle immediately, even while the uncancellable transition it interrupted is still settling
hasSucceededbooleanLast execution completed without errors (a void action succeeds with result.data still undefined)
hasErroredbooleanLast execution had serverError, validationErrors, or threw (a raw throw leaves result empty)
hasNavigatedbooleanLast 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 |

useOptimisticStateAction returns everything useStateAction returns, plus: | optimisticState | State | Confirmed state folded with every in-flight change (always defined) |

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.

initResult Option

All hooks accept initResult to seed the hook with a preloaded result (e.g. data fetched on the server): in the opts object for useAction/useStateAction, in the utils object (alongside currentState/updateFn) for useOptimisticAction/useOptimisticStateAction. The value is captured once at mount (like React's useActionState initial state): later changes to the option are ignored, and reset() restores the mount value. The seeded shape precisely types the idle branch's result. See initResult in depth.

Supporting Docs

Anti-Patterns

ts
// 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;
  }
};
tsx
// BAD: Last-write-wins for changes that accumulate — the second move builds on stale state,
// and the response of the first move is discarded
const { execute } = useOptimisticAction(moveItem, { currentState: items, updateFn: reorder });

// GOOD: Queued dispatches, each folding over the previous result
const { execute } = useOptimisticStateAction(moveItem, { currentState: items, updateFn: reorder });
ts
// 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!
do mesmo repositório

Mais Skills

Todos os Skills