This adapter offers a way to seamlessly integrate next-safe-action with react-hook-form.
- React >=
18.2.0 - Next.js >=
14.0.0 - next-safe-action >=
7.6.0 - react-hook-form >=
7.0.0 - @hookform/resolvers >=
3.0.0
npm i next-safe-action react-hook-form @hookform/resolvers @next-safe-action/adapter-react-hook-formThe best way to learn how to use this adapter is to take a look at the examples. The app in this repository shows you how to use the useHookFormAction and useHookFormOptimisticAction hooks:
This hook is a wrapper around useAction from next-safe-action and useForm from react-hook-form that makes it much easier to use safe actions with react-hook-form. It also maps validation errors to FieldErrors compatible with react-hook-form.
- First of all, we need a shared file to store our validation schema(s). In this case, the
loginSchemaZod validator is exported fromvalidation.ts:
import { z } from "zod";
export const loginSchema = z.object({
username: z.string().min(3).max(30),
password: z.string().min(8).max(100),
});- Then, we can create our login action using
loginSchema:
"use server";
import { returnValidationErrors } from "next-safe-action";
import { actionClient } from "@/lib/safe-action";
import { loginSchema } from "./validation";
import { checkCredentials } from "@/services/auth";
export const loginAction = actionClient.schema(loginSchema).action(async ({ parsedInput }) => {
const valid = await checkCredentials(parsedInput.username, parsedInput.password);
// If the credentials are invalid, return root validation error.
if (!valid) {
returnValidationErrors(loginSchema, {
_errors: ["Invalid username or password"],
});
}
return {
successful: true,
};
});- Finally, we can use
useHookFormActionin our Client Component, by passing to it theloginSchemaandloginActiondeclared above:
"use client";
import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks";
import { zodResolver } from "@hookform/resolvers/zod";
import { loginSchema } from "./validation";
import { loginAction } from "./login-action";
export function LoginForm() {
const { form, action, handleSubmitWithAction, resetFormAndAction } = useHookFormAction(
loginAction,
zodResolver(loginSchema),
{
actionProps: {},
formProps: {},
errorMapProps: {},
}
);
return <form onSubmit={handleSubmitWithAction}>...</form>;
}safeAction: the safe action (required)hookFormResolver: a react-hook-form validation resolver (required)props: props foruseAction,useFormand error mapper (optional)
form: the react-hook-form formaction: the next-safe-action actionhandleSubmitWithAction: a function that handles form submission by automatically executing the actionresetFormAndAction: a function that resets the form and the action state
This hook is a wrapper around useOptimisticAction from next-safe-action and useForm from react-hook-form that makes it much easier to use safe actions with react-hook-form. It also maps validation errors to FieldErrors compatible with react-hook-form.
- First of all, we need a shared file to store our validation schema(s). In this case, the
addTodoSchemaZod validator is exported fromvalidation.ts:
import { z } from "zod";
export const addTodoSchema = z.object({
newTodo: z.string().min(1).max(200),
});- Then, we can create our add todo action using
addTodoSchema:
"use server";
import { returnValidationErrors } from "next-safe-action";
import { revalidatePath } from "next/cache";
import { actionClient } from "@/lib/safe-action";
import { addTodoSchema } from "./validation";
import { badWordsCheck } from "@/utils";
import { saveTodoInDb } from "@/services/db";
export const addTodoAction = actionClient.schema(addTodoSchema).action(async ({ parsedInput }) => {
const containsBadWords = badWordsCheck(parsedInput.newTodo);
// If the todo con
if (containsBadWords) {
returnValidationErrors(addTodoSchema, {
newTodo: {
_errors: ["The todo contains bad words!"],
},
});
}
await saveTodoInDb(parsedInput.newTodo);
revalidatePath("/");
return {
newTodo: parsedInput.newTodo,
};
});- Finally, we can use
useHookFormOptimisticActionin our Client Component, by passing to it theaddTodoSchemaandaddTodoActiondeclared above:
"use client";
import { useHookFormOptimisticAction } from "@next-safe-action/adapter-react-hook-form/hooks";
import { zodResolver } from "@hookform/resolvers/zod";
import { addTodoSchema } from "./validation";
import { addTodoAction } from "./addtodo-action";
type Props = {
todos: string[];
};
// Todos are passed from the parent Server Component and updated each time a new todo is added
// thanks to the `revalidatePath` function called inside the action.
export function AddTodoForm({ todos }: Props) {
const { form, action, handleActionSubmit, resetFormAndAction } = useHookFormOptimisticAction(
addTodoAction,
zodResolver(addTodoSchema),
{
actionProps: {
currentState: {
todos,
},
updateFn: (state, input) => {
return {
todos: [...state.todos, input.newTodo],
};
},
},
formProps: {},
errorMapProps: {},
}
);
return <form onSubmit={handleActionSubmit}></form>;
}safeAction: the safe action (required)hookFormResolver: a react-hook-form validation resolver (required)props: props foruseOptimisticAction,useFormand error mapper.actionProps.currentStateandactionProps.updateFnare required by theuseOptimisticActionhook used under the hood, the rest are optional. (required/optional)
form: the react-hook-form formaction: the next-safe-action actionhandleSubmitWithAction: a function that handles form submission by automatically executing the actionresetFormAndAction: a function that resets the form and the action state
For more control over the execution flow, you can use this hook to get back the memoized mapped validation errors of the action. It can be useful for cases when you need to use both useAction and useForm in your Client Component, for a particular task, or when you want to create custom hooks.
-
We'll reuse the
loginSchemaandloginActionfrom theuseHookFormActionexample here. -
Here's how you would use
useHookFormActionErrorMapperin your Client Component:
"use client";
import { useHookFormActionErrorMapper } from "@next-safe-action/adapter-react-hook-form/hooks";
import { zodResolver } from "@hookform/resolvers/zod";
import { loginSchema } from "./validation";
import { loginAction } from "./login-action";
import { useAction } from "next-safe-action/hooks";
import { useForm } from "react-hook-form";
import { Infer } from "next-safe-action/adapters/types";
export function CustomForm() {
const action = useAction(loginAction);
const { hookFormValidationErrors } = useHookFormActionErrorMapper<typeof loginSchema>(
action.result.validationErrors,
{ joinBy: "\n" }
);
const form = useForm<Infer<typeof loginSchema>>({
resolver: zodResolver(loginSchema),
errors: hookFormValidationErrors,
});
return <form onSubmit={form.handleSubmit(action.executeAsync)}>...</form>;
}validationErrors: next-safe-action object ofValidationErrors, orundefined(required)props:joinByfromErrorMapperPropstype. It's used to determine how to join the error messages, if more than one is present in the errors array. It defaults to" "(optional)
hookFormValidationErrors: object of mapped errors withFieldErrorstype, compatible with react-hook-form
For more advanced stuff, you can directly use the mapToHookFormErrors function that is utilized under the hood to map next-safe-action ValidationErrors to react-hook-form compatible FieldErrors.
import { mapToHookFormErrors } from "@next-safe-action/adapter-react-hook-form";
import { loginAction } from "./login-action";
import type { loginSchema } from "./validation";
async function advancedStuff() {
const result = await loginAction({ username: "foo", password: "bar" });
const hookFormValidationErrors = mapToHookFormErrors<typeof loginSchema>(result?.validationErrors, { joinBy: "\n" });
// Do something with `hookFormValidationErrors`...
}validationErrors: next-safe-action object ofValidationErrors, orundefined(required)props:joinByfromErrorMapperPropstype. It's used to determine how to join the error messages, if more than one is present in the errors array. It defaults to" "(optional)
- mapped errors: object of mapped errors with
FieldErrorstype, compatible with react-hook-form
Props for mapToHookFormErrors. Also used by the hooks.
export type ErrorMapperProps = {
joinBy?: string;
};Optional props for useHookFormAction and useHookFormOptimisticAction.
export type HookProps<ServerError, S extends StandardSchemaV1 | undefined, CVE, Data, FormContext = any> = {
errorMapProps?: ErrorMapperProps;
actionProps?: HookCallbacks<ServerError, S, CVE, Data>;
formProps?: Omit<UseFormProps<InferInputOrDefault<S, any>, FormContext, InferOutputOrDefault<S, any>>, "resolver">;
};Type of the return object of the useHookFormAction hook.
export type UseHookFormActionHookReturn<
ServerError,
S extends StandardSchemaV1 | undefined,
CVE,
Data,
FormContext = any,
> = {
action: UseActionHookReturn<ServerError, S, CVE, Data>;
form: UseFormReturn<InferInputOrDefault<S, any>, FormContext, InferOutputOrDefault<S, any>>;
handleSubmitWithAction: (e?: React.BaseSyntheticEvent) => Promise<void>;
resetFormAndAction: () => void;
};Type of the return object of the useHookFormOptimisticAction hook.
export type UseHookFormOptimisticActionHookReturn<
ServerError,
S extends StandardSchemaV1 | undefined,
CVE,
Data,
State,
FormContext = any,
> = Omit<UseHookFormActionHookReturn<ServerError, S, CVE, Data, FormContext>, "action"> & {
action: UseOptimisticActionHookReturn<ServerError, S, CVE, Data, State>;
};You can use these utility types exported from the /hooks path to infer the return types of the hooks.
Infer the type of the return object of the useHookFormAction hook.
export type InferUseHookFormActionHookReturn<T extends Function, FormContext = any> =
T extends SafeActionFn<
infer ServerError,
infer S extends Schema | undefined,
infer BAS extends readonly Schema[],
infer CVE,
infer CBAVE,
infer Data
>
? UseHookFormActionHookReturn<ServerError, S, BAS, CVE, CBAVE, Data, FormContext>
: never;Infer the type of the return object of the useHookFormOptimisticAction hook.
export type InferUseHookFormOptimisticActionHookReturn<T extends Function, State, FormContext = any> =
T extends SafeActionFn<
infer ServerError,
infer S extends Schema | undefined,
infer BAS extends readonly Schema[],
infer CVE,
infer CBAVE,
infer Data
>
? UseHookFormOptimisticActionHookReturn<ServerError, S, BAS, CVE, CBAVE, Data, State, FormContext>
: never;This project is released under the MIT License.