Last active
December 22, 2023 16:28
-
-
Save pom421/ea34eeb778b0d94fe85352dc27aada96 to your computer and use it in GitHub Desktop.
Form with React Hook form and zod rules (Next.js page example)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// try it : https://codesandbox.io/s/sample-next-ts-rhf-zod-9ieev | |
import React from "react"; | |
import { useForm } from "react-hook-form"; | |
import { zodResolver } from "@hookform/resolvers/zod"; | |
import { z } from "zod"; | |
import type { FieldError } from "react-hook-form"; | |
// JSON.stringify(error) will not work, because of circulare structure. So we need this helper. | |
const formatErrors = (errors: Record<string, FieldError>) => | |
Object.keys(errors).map((key) => ({ | |
key, | |
message: errors[key].message | |
})); | |
/* ---------- Some UI components ----------*/ | |
type AlertType = "error" | "warning" | "success"; | |
// Global Alert div. | |
const Alert = ({ children, type }: { children: string; type: AlertType }) => { | |
const backgroundColor = | |
type === "error" ? "tomato" : type === "warning" ? "orange" : "powderBlue"; | |
return <div style={{ padding: "0 10", backgroundColor }}>{children}</div>; | |
}; | |
// Use role="alert" to announce the error message. | |
const AlertInput = ({ children }: { children: React.ReactNode }) => | |
Boolean(children) ? ( | |
<span role="alert" style={{ color: "tomato" }}> | |
{children} | |
</span> | |
) : null; | |
/* ---------- Zod schema et TS type ----------*/ | |
const titles = ["Mr", "Mrs", "Miss", "Dr"] as const; // as const is mandatory to get litteral types in UserType. | |
// Describe the correctness of data's form. | |
const userSchema = z | |
.object({ | |
firstName: z.string().max(36), | |
lastName: z | |
.string() | |
.min(1, { message: "The lastName is required." }) | |
.max(36), | |
mobileNumber: z.string().min(10).max(13).optional(), | |
email: z | |
.string() | |
.min(1, "The email is required.") | |
.email({ message: "The email is invalid." }), | |
confirmEmail: z.string().min(1, "The email is required."), | |
// At first, no option radio are checked so this is null. So the error is "Expected string, received null". | |
// So we need to accept first string or null, in order to apply refine to set a custom message. | |
isDeveloper: z.union([z.string(), z.null()]).refine((val) => val !== null, { | |
message: "Please, make a choice!" | |
}), | |
title: z.enum(titles), | |
age: z | |
.number({ invalid_type_error: "Un nombre est attendu" }) | |
.int() | |
.refine((val) => val >= 18, { message: "Vous devez être majeur" }) | |
}) | |
// The refine method is used to add custom rules or rules over multiple fields. | |
.refine((data) => data.email === data.confirmEmail, { | |
message: "Emails don't match.", | |
path: ["confirmEmail"] // Set the path of this error on the confirmEmail field. | |
}); | |
// Infer the TS type according to the zod schema. | |
type UserType = z.infer<typeof userSchema>; | |
/* ---------- React component ----------*/ | |
export default function App() { | |
const { | |
register, | |
handleSubmit, | |
watch, | |
formState: { errors, isSubmitting, isSubmitted, isDirty, isValid } | |
} = useForm<UserType>({ | |
mode: "onChange", | |
resolver: zodResolver(userSchema), // Configuration the validation with the zod schema. | |
defaultValues: { | |
isDeveloper: undefined, | |
mobileNumber: "666-666666", | |
firstName: "toto", | |
lastName: "titi", | |
email: "", | |
confirmEmail: "", | |
title: "Miss" | |
} | |
}); | |
// The onSubmit function is invoked by RHF only if the validation is OK. | |
const onSubmit = (user: UserType) => { | |
console.log("dans onSubmit", user); | |
}; | |
return ( | |
<> | |
<h1>Ajout d'un utilisateur</h1> | |
<p style={{ fontStyle: "italic", maxWidth: 600 }}> | |
This example is a demo to show the use of a form, driven with React Hook | |
Form and validated by zod. The example is in full Typescript. | |
</p> | |
{Boolean(Object.keys(errors)?.length) && ( | |
<Alert type="error">There are errors in the form.</Alert> | |
)} | |
<form | |
onSubmit={handleSubmit(onSubmit)} | |
style={{ display: "flex", flexDirection: "column", maxWidth: 600 }} | |
noValidate | |
> | |
{/* use aria-invalid to indicate field contain error for accessiblity reasons. */} | |
<input | |
type="text" | |
placeholder="First name is not mandatory" | |
{...register("firstName")} | |
aria-invalid={Boolean(errors.firstName)} | |
/> | |
<AlertInput>{errors?.firstName?.message}</AlertInput> | |
<input | |
type="text" | |
placeholder="Last name (mandatory)" | |
{...register("lastName")} | |
aria-invalid={Boolean(errors.lastName)} | |
/> | |
<AlertInput>{errors?.lastName?.message}</AlertInput> | |
<input | |
type="text" | |
placeholder="Email (mandatory)" | |
{...register("email")} | |
aria-invalid={Boolean(errors.email)} | |
/> | |
<AlertInput>{errors?.email?.message}</AlertInput> | |
<input | |
type="text" | |
placeholder="The same email as above" | |
{...register("confirmEmail")} | |
aria-invalid={Boolean(errors.confirmEmail)} | |
/> | |
<AlertInput>{errors?.confirmEmail?.message}</AlertInput> | |
<input | |
type="tel" | |
placeholder="Mobile number (mandatory)" | |
{...register("mobileNumber")} | |
aria-invalid={Boolean(errors.mobileNumber)} | |
/> | |
<AlertInput>{errors?.mobileNumber?.message}</AlertInput> | |
<select {...register("title")} aria-invalid={Boolean(errors.title)}> | |
{titles.map((elt) => ( | |
<option key={elt} value={elt}> | |
{elt} | |
</option> | |
))} | |
</select> | |
<label> | |
Âge | |
<input | |
type="number" | |
placeholder="Age" | |
{...register("age", { valueAsNumber: true })} | |
aria-invalid={Boolean(errors.age)} | |
/> | |
</label> | |
<AlertInput>{errors?.age?.message}</AlertInput> | |
<div> | |
<p>Are you a developer? (mandatory)</p> | |
<label> | |
Yes | |
<input {...register("isDeveloper")} type="radio" value="Yes" /> | |
</label> | |
</div> | |
<div> | |
<label> | |
No | |
<input {...register("isDeveloper")} type="radio" value="No" /> | |
</label> | |
</div> | |
<AlertInput>{errors?.isDeveloper?.message}</AlertInput> | |
<input type="submit" disabled={isSubmitting || !isValid} /> | |
<pre>{JSON.stringify(formatErrors, null, 2)}</pre> | |
<pre>{JSON.stringify(watch(), null, 2)}</pre> | |
<pre> | |
formState ={" "} | |
{JSON.stringify( | |
{ isSubmitting, isSubmitted, isDirty, isValid }, | |
null, | |
2 | |
)} | |
</pre> | |
</form> | |
</> | |
); | |
} |
nice example @pom421 , kinda offtopic, do you know how to make this work with trpc?
I'm getting this error:
Error: You're trying to use @trpc/server in a non-server environment. This is not supported by default.
at initTRPCInner (webpack-internal:///./node_modules/@trpc/server/dist/index.mjs:823:23)
at TRPCBuilder.create (webpack-internal:///./node_modules/@trpc/server/dist/index.mjs:797:33)
at eval (webpack-internal:///./src/server/trpc/trpc.ts:11:72)
at ./src/server/trpc/trpc.ts (http://localhost:3000/_next/static/webpack/pages/clients/new.c94d29ea0a4cdf91.hot-update.js:38:1)
This is my code:
import { useForm } from "react-hook-form";
import { trpc } from "../../utils/trpc";
import { zodResolver } from "@hookform/resolvers/zod";
import { clientZod } from "../../server/trpc/router/client";
import type { ClientType } from "../../server/trpc/router/client";
const CreateClientPage = () => {
const { handleSubmit, register } = useForm<ClientType>({
resolver: zodResolver(clientZod),
});
const { mutate } = trpc.client.create.useMutation();
const onSubmit = (values: ClientType) => mutate(values);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" placeholder="Client name" {...register("name")} />
<input type="text" placeholder="Client email" {...register("email")} />
<input
type="text"
placeholder="Client website"
{...register("website")}
/>
<input type="submit" value={"Create"} />
</form>
);
};
export default CreateClientPage;
@llermaly you have to define your validation schema (clientZod
) in file other than your router and the error should gone. I just came into same.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@iltonbarbosa
With well written libraries, you just need to inject register. See this example in Chakra UI : https://chakra-ui.com/getting-started/with-hook-form.
Otherwise, you need to make a forwardRef wrapper of your component or use control API of React Hook Form. See https://react-hook-form.com/get-started/#Integratinganexistingform.