skills/next-safe-action/skills/safe-action-hooks

safe-action-hooks

SKILL.md

next-safe-action React Hooks

Import

// Standard hooks
import { useAction, useOptimisticAction } from "next-safe-action/hooks";

// Deprecated — use React's useActionState directly instead
import { useStateAction } from "next-safe-action/stateful-hooks";

useAction — Quick Start

"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

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

Return Value

Both useAction and useOptimisticAction 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 ({ data?, serverError?, validationErrors? })
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 |

Supporting Docs

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) {
    // Navigation errors (redirect, notFound) are re-thrown
    // They'll be handled by Next.js — just let them propagate
    throw e;
  }
};
Weekly Installs
32
First Seen
10 days ago
Installed on
opencode32
gemini-cli32
github-copilot32
codex32
amp32
kimi-cli32