next-safe-action
Integrations

React Hook Form

next-safe-action has a first-party adapter for React Hook Form that provides seamless integration between validated server actions and form state management.

Installation

npm install next-safe-action react-hook-form @hookform/resolvers @next-safe-action/adapter-react-hook-form

Quick start

Define the action

Create a standard server action with an input schema:

src/app/actions.ts
"use server";

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

const createUserSchema = z.object({
	name: z.string().min(2, "Name must be at least 2 characters"),
	email: z.string().email("Invalid email address"),
	age: z.coerce.number().min(18, "Must be at least 18"),
});

export const createUser = actionClient
	.inputSchema(createUserSchema)
	.action(async ({ parsedInput }) => {
		const user = await db.user.create({ data: parsedInput });
		return { user };
	});

Create the form component

Use the useHookFormAction hook from the adapter to connect your action with React Hook Form:

src/app/create-user-form.tsx
"use client";

import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";
import { zodResolver } from "@hookform/resolvers/zod";
import { createUser } from "./actions";
import { z } from "zod";

const schema = z.object({
	name: z.string().min(2, "Name must be at least 2 characters"),
	email: z.string().email("Invalid email address"),
	age: z.coerce.number().min(18, "Must be at least 18"),
});

export function CreateUserForm() {
	const { form, action, handleSubmitWithAction, resetFormAndAction } =
		useHookFormAction(createUser, zodResolver(schema), {
			formProps: {
				defaultValues: {
					name: "",
					email: "",
					age: 18,
				},
			},
			actionProps: {
				onSuccess: ({ data }) => {
					alert(`Created user: ${data.user.name}`);
					resetFormAndAction();
				},
			},
		});

	return (
		<form onSubmit={handleSubmitWithAction}>
			<div>
				<label>Name</label>
				<input {...form.register("name")} />
				{form.formState.errors.name && (
					<p>{form.formState.errors.name.message}</p>
				)}
			</div>

			<div>
				<label>Email</label>
				<input {...form.register("email")} />
				{form.formState.errors.email && (
					<p>{form.formState.errors.email.message}</p>
				)}
			</div>

			<div>
				<label>Age</label>
				<input type="number" {...form.register("age")} />
				{form.formState.errors.age && (
					<p>{form.formState.errors.age.message}</p>
				)}
			</div>

			{action.result.serverError && (
				<p>{action.result.serverError}</p>
			)}

			<button type="submit" disabled={action.isPending}>
				{action.isPending ? "Creating..." : "Create User"}
			</button>
		</form>
	);
}

How it works

The adapter bridges two concerns:

  1. Client-side validation: React Hook Form validates the form using the resolver (zodResolver, valibotResolver, etc.) for instant feedback
  2. Server-side validation: next-safe-action validates the same data on the server for security

When the form is submitted:

  1. React Hook Form validates client-side first
  2. If valid, the data is sent to the server action
  3. Server-side validation errors are automatically mapped back to the form fields via react-hook-form's errors prop

Package entry points

The adapter is split into two entry points:

Entry pointExportsEnvironment
@next-safe-action/adapter-react-hook-formmapToHookFormErrors, ErrorMapperPropsServer & Client
@next-safe-action/adapter-react-hook-form/hooksuseHookFormAction, useHookFormOptimisticAction, useHookFormActionErrorMapperClient only

useHookFormAction

The primary hook for using safe actions with React Hook Form. It combines useAction and useForm into a single hook, automatically mapping server validation errors to form field errors.

import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";

const { form, action, handleSubmitWithAction, resetFormAndAction } =
	useHookFormAction(safeAction, hookFormResolver, props?);

Parameters

Prop

Type

Props

The optional props object has the following shape:

Prop

Type

Return object

Prop

Type

Example

See the Quick start section above for a full example.


useHookFormOptimisticAction

Combines useOptimisticAction and useForm into a single hook. Use this when you want optimistic UI updates — the form's result updates immediately before the server responds, then reverts if the action fails.

import { useHookFormOptimisticAction } from "@next-safe-action/adapter-react-hook-form/hooks";

const { form, action, handleSubmitWithAction, resetFormAndAction } =
	useHookFormOptimisticAction(safeAction, hookFormResolver, props);

Parameters

Prop

Type

Props

Same as useHookFormAction props, with the following required additions in actionProps:

Prop

Type

Return object

Same as useHookFormAction, but action is the return type of useOptimisticAction instead of useAction. This means action also includes optimisticState:

Prop

Type

Example

Define the action

src/app/actions.ts
"use server";

import { z } from "zod";
import { revalidatePath } from "next/cache";
import { actionClient } from "@/lib/safe-action";

export type Item = { id: string; name: string };

const addItemSchema = z.object({
	name: z.string().min(1, "Name is required").max(50),
});

export const addItem = actionClient
	.inputSchema(addItemSchema)
	.action(async ({ parsedInput }) => {
		const item = { ...parsedInput, id: crypto.randomUUID() };
		await db.item.create({ data: item });
		revalidatePath("/items");
		return { newItem: item };
	});

Create the Server Component

src/app/items/page.tsx
import { db } from "@/lib/db";
import { ItemForm } from "./item-form";

export default async function ItemsPage() {
	const items = await db.item.findMany();
	return <ItemForm items={items} />;
}

Create the Client Component with optimistic updates

src/app/items/item-form.tsx
"use client";

import { useHookFormOptimisticAction } from "@next-safe-action/adapter-react-hook-form/hooks";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import type { Item } from "./actions";
import { addItem } from "./actions";

const schema = z.object({
	name: z.string().min(1, "Name is required").max(50),
});

export function ItemForm({ items }: { items: Item[] }) {
	const { form, action, handleSubmitWithAction, resetFormAndAction } =
		useHookFormOptimisticAction(addItem, zodResolver(schema), {
			actionProps: {
				currentState: { items },
				updateFn: (state, input) => ({
					items: [...state.items, { ...input, id: crypto.randomUUID() }],
				}),
				onSuccess() {
					form.reset();
				},
			},
			formProps: {
				defaultValues: {
					name: "",
				},
			},
		});

	return (
		<div>
			<form onSubmit={handleSubmitWithAction}>
				<input {...form.register("name")} placeholder="Item name" />
				{form.formState.errors.name && (
					<p>{form.formState.errors.name.message}</p>
				)}
				<button type="submit" disabled={action.isPending}>
					{action.isPending ? "Adding..." : "Add item"}
				</button>
			</form>

			<ul>
				{action.optimisticState.items.map((item) => (
					<li key={item.id}>{item.name}</li>
				))}
			</ul>
		</div>
	);
}

useHookFormActionErrorMapper

A lower-level hook for advanced use cases where you want full control over useAction and useForm separately. It takes a validation errors object and returns react-hook-form-compatible FieldErrors that you can pass to useForm's errors prop.

import { useHookFormActionErrorMapper } from "@next-safe-action/adapter-react-hook-form/hooks";

const { hookFormValidationErrors } = useHookFormActionErrorMapper(validationErrors, props?);

Parameters

Prop

Type

Return object

Prop

Type

Example

src/app/buy-product-form.tsx
"use client";

import { useHookFormActionErrorMapper } from "@next-safe-action/adapter-react-hook-form/hooks";
import { zodResolver } from "@hookform/resolvers/zod";
import { useAction } from "next-safe-action/hooks";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { buyProduct } from "./actions";

const schema = z.object({
	productId: z.string().min(1, "Product ID is required"),
	quantity: z.coerce.number().min(1, "Must order at least 1"),
});

export function BuyProductForm() {
	// Step 1: Use the action hook separately
	const {
		execute,
		result,
		status,
		reset: resetAction,
		isPending,
	} = useAction(buyProduct);

	// Step 2: Map server validation errors to react-hook-form format
	const { hookFormValidationErrors } = useHookFormActionErrorMapper(
		result.validationErrors
	);

	// Step 3: Use the form hook separately, passing mapped errors
	const {
		register,
		handleSubmit,
		reset: resetForm,
		formState: { errors },
	} = useForm({
		resolver: zodResolver(schema),
		errors: hookFormValidationErrors,
		defaultValues: {
			productId: "",
			quantity: 1,
		},
	});

	return (
		<form onSubmit={handleSubmit((data) => execute(data))}>
			<div>
				<input {...register("productId")} placeholder="Product ID" />
				{errors.productId && <p>{errors.productId.message}</p>}
			</div>
			<div>
				<input type="number" {...register("quantity")} />
				{errors.quantity && <p>{errors.quantity.message}</p>}
			</div>
			<button type="submit" disabled={isPending}>
				{isPending ? "Purchasing..." : "Buy product"}
			</button>
			<button
				type="button"
				onClick={() => {
					resetForm();
					resetAction();
				}}
			>
				Reset
			</button>
		</form>
	);
}

This pattern is useful when you need to customize the action hook behavior (e.g. use execute instead of executeAsync), add logic between form submission and action execution, or integrate with existing useForm setups.


mapToHookFormErrors

A non-hook utility function that maps next-safe-action validation errors to react-hook-form FieldErrors. This is the underlying function used by useHookFormActionErrorMapper (which wraps it in useMemo).

import { mapToHookFormErrors } from "@next-safe-action/adapter-react-hook-form";

Parameters

Prop

Type

Return value

Returns a FieldErrors object compatible with react-hook-form, or undefined if there are no validation errors. Each field error has type: "validate" and a message string.

Example

import { mapToHookFormErrors } from "@next-safe-action/adapter-react-hook-form";

const validationErrors = {
	email: { _errors: ["Invalid email", "Email already taken"] },
	name: { _errors: ["Too short"] },
};

const fieldErrors = mapToHookFormErrors(validationErrors);
// → { email: { type: "validate", message: "Invalid email Email already taken" }, name: { type: "validate", message: "Too short" } }

// Use joinBy to customize how multiple errors are joined
const fieldErrors2 = mapToHookFormErrors(validationErrors, { joinBy: ", " });
// → { email: { type: "validate", message: "Invalid email, Email already taken" }, ... }

Type utilities

The adapter exports two utility types for inferring the return type of the hooks from an action. These are useful when you need to type a variable or prop that holds the hook's return value.

InferUseHookFormActionHookReturn

Infer the return type of useHookFormAction from a safe action function:

import type { InferUseHookFormActionHookReturn } from "@next-safe-action/adapter-react-hook-form/hooks";

// Given a safe action:
const myAction = actionClient.inputSchema(schema).action(async ({ parsedInput }) => {
	return { success: true };
});

// Infer the hook return type:
type MyHookReturn = InferUseHookFormActionHookReturn<typeof myAction>;

InferUseHookFormOptimisticActionHookReturn

Infer the return type of useHookFormOptimisticAction from a safe action function and a state type:

import type { InferUseHookFormOptimisticActionHookReturn } from "@next-safe-action/adapter-react-hook-form/hooks";

type MyState = { items: Item[] };
type MyOptimisticHookReturn = InferUseHookFormOptimisticActionHookReturn<typeof myAction, MyState>;

See also

On this page