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> | |
</> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@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.