Safe Action Forms

next-safe-action/skills/skills/safe-action-forms

by next-safe-actiona2605bd2e84321245cba8f5718c144a6e4a5fa47No licenseListed Oct 9, 2026Updated Oct 9, 2026

Use when integrating next-safe-action with forms -- react-hook-form adapter (useHookFormAction, useHookFormOptimisticAction, mapToHookFormErrors), native HTML forms, bind arguments, or file uploads

Instructions onlySoftware Development
AI-generated overview

Guides integrating next-safe-action with React forms, including native forms, the react-hook-form adapter and file uploads.

What it does
Explains how to connect next-safe-action server actions to forms in a React/Next.js app. It compares approaches such as useAction with native forms, useStateAction with form action, useHookFormAction and useHookFormOptimisticAction, and shows code samples for each. It also covers validation error display, server errors, pending state, bind arguments and file uploads, and lists the adapter package entry points and exports.
When to use it
Use when building or reviewing forms in a Next.js project that uses next-safe-action and you need to choose a form integration approach. It fits cases involving field-level validation errors, optimistic updates, native form submission or file uploads. It is not for general React form design outside next-safe-action.
Requirements
No scripts; instructions only. Assumes a React/Next.js project using next-safe-action, and for the React Hook Form path the @next-safe-action/adapter-react-hook-form package, react-hook-form, a resolver such as @hookform/resolvers/zod and zod.

next-safe-action Form Integration

Options

ApproachWhen to Use
useAction + native formSimple forms, no complex validation UI, programmatic triggers
useStateAction + <form action={formAction}>Forms with state tracking, need prevResult access, full callbacks
useHookFormAction (RHF adapter)Complex forms with field-level errors, validation on change/blur
useHookFormOptimisticActionRHF forms with optimistic UI updates
useOptimisticStateActionInstant UI where overlapping writes must be queued and accumulate (see the hooks skill)

Quick Start — useStateAction Form

tsx
"use client";
import { useStateAction } from "next-safe-action/hooks";import { submitContact } from "@/app/actions";
export function ContactForm() {  const { formAction, result, isPending, hasSucceeded } = useStateAction(submitContact, {    onSuccess: () => toast.success("Message sent!"),  });
  return (    <form action={formAction}>      <input name="name" required />      <input name="email" type="email" required />      <textarea name="message" required />
      {result.validationErrors?.email && (        <p>{result.validationErrors.email._errors?.[0]}</p>      )}      {result.serverError && <p>{result.serverError}</p>}      {hasSucceeded && <p>Message sent!</p>}
      <button type="submit" disabled={isPending}>        {isPending ? "Sending..." : "Send"}      </button>    </form>  );}

Note: useStateAction requires the server action to be defined with .stateAction() instead of .action(), and <form action={formAction}> passes raw FormData, so its input schema must parse FormData (e.g. zfd.formData({...}) from zod-form-data). See the hooks skill for the full decision table on when to use useAction vs useStateAction.

Quick Start — Native Form

tsx
"use client";
import { useAction } from "next-safe-action/hooks";import { submitContact } from "@/app/actions";
export function ContactForm() {  const { execute, result, isPending } = useAction(submitContact);
  return (    <form      onSubmit={(e) => {        e.preventDefault();        const fd = new FormData(e.currentTarget);        execute({          name: fd.get("name") as string,          email: fd.get("email") as string,          message: fd.get("message") as string,        });      }}    >      <input name="name" required />      <input name="email" type="email" required />      <textarea name="message" required />
      {result.validationErrors && (        <p>{result.validationErrors.email?._errors?.[0]}</p>      )}      {result.serverError && <p>{result.serverError}</p>}      {result.data && <p>Message sent!</p>}
      <button type="submit" disabled={isPending}>        {isPending ? "Sending..." : "Send"}      </button>    </form>  );}

Quick Start — React Hook Form Adapter

tsx
"use client";
import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";import { zodResolver } from "@hookform/resolvers/zod";import { z } from "zod";import { submitContact } from "@/app/actions";
const schema = z.object({  name: z.string().min(1, "Name is required"),  email: z.string().email("Invalid email"),  message: z.string().min(10, "Message must be at least 10 characters"),});
export function ContactForm() {  const { form, handleSubmitWithAction, action } = useHookFormAction(    submitContact,    zodResolver(schema),    {      actionProps: {        onSuccess: () => toast.success("Message sent!"),      },    }  );
  return (    <form onSubmit={handleSubmitWithAction}>      <input {...form.register("name")} />      {form.formState.errors.name && <p>{form.formState.errors.name.message}</p>}
      <input {...form.register("email")} />      {form.formState.errors.email && <p>{form.formState.errors.email.message}</p>}
      <textarea {...form.register("message")} />      {form.formState.errors.message && <p>{form.formState.errors.message.message}</p>}
      {action.result.serverError && <p>{action.result.serverError}</p>}
      <button type="submit" disabled={action.isPending}>        {action.isPending ? "Sending..." : "Send"}      </button>    </form>  );}

Supporting Docs

  • Native form submission patterns
  • React Hook Form adapter in depth
  • File uploads

Entry Points

PackageEntry PointExports
@next-safe-action/adapter-react-hook-formDefaultmapToHookFormErrors, types
@next-safe-action/adapter-react-hook-form/hooksHooksuseHookFormAction, useHookFormOptimisticAction, useHookFormActionErrorMapper

Source and attribution

Source:next-safe-action/skillsinskills/safe-action-formsat commita2605bd

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal