safe-action-forms
Use when integrating next-safe-action with forms -- react-hook-form adapter (useHookFormAction, useHookFormOptimisticAction, mapToHookFormErrors), native HTML forms, bind arguments, or file uploads
Install
npx skills add https://github.com/next-safe-action/skills --skill safe-action-formsSKILL.md
next-safe-action Form Integration
Options
| Approach | When to Use |
|---|---|
useAction + native form |
Simple 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 |
useHookFormOptimisticAction |
RHF forms with optimistic UI updates |
useOptimisticStateAction |
Instant UI where overlapping writes must be queued and accumulate (see the hooks skill) |
Quick Start — useStateAction Form
"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
"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
"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
Entry Points
| Package | Entry Point | Exports |
|---|---|---|
@next-safe-action/adapter-react-hook-form |
Default | mapToHookFormErrors, types |
@next-safe-action/adapter-react-hook-form/hooks |
Hooks | useHookFormAction, useHookFormOptimisticAction, useHookFormActionErrorMapper |
Related skills
vercel-react-best-practicesvercel-labs751KReact and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.vercel-composition-patternsvercel-labs361KReact composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component libraries, or designing reusable APIs. Triggers on tasks involving compound components, render props, context providers, or component architecture. Includes React 19 API changes.vercel-react-view-transitionsvercel-labs135KGuide for implementing smooth, native-feeling animations using React's View Transition API (`<ViewTransition>` component, `addTransitionType`, and CSS view transition pseudo-elements). Use this skill whenever the user wants to add page transitions, animate route changes, create shared element animations, animate enter/exit of components, animate list reorder, implement directional (forward/back) navigation animations, or integrate view transitions in Next.js. Also use when the user mentions viewpick-ui-libraryemilkowalski124KPick the right library for a given frontend task from a curated, opinionated list — numbers, OTP inputs, charts, command menus, virtualization, drag and drop, toasts, state, styling, and more. Only runs when explicitly invoked; it does not trigger on its own.