-
-
Save Liopun/f5c9efad7eaa35c710421dd5d658629a to your computer and use it in GitHub Desktop.
Form with React Hook form and zod rules (Next.js page example)
This file contains hidden or 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" | |
| /* ---------- Some UI components ----------*/ | |
| type AlertType = "error" | "warning" | "success" | |
| // Global Alert div. | |
| function 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 }) => | |
| Boolean(children) ? ( | |
| <span role="alert" style={{ color: "tomato" }}> | |
| {children} | |
| </span> | |
| ) : null | |
| /* ---------- Zod schema et TS type ----------*/ | |
| // Describe the correctness of data's form. | |
| const userSchema = z | |
| .object({ | |
| firstName: z.string().max(36), | |
| lastName: z.string().nonempty({ message: "The lastName is required." }).max(36), | |
| mobileNumber: z.string().min(10).max(13).optional(), | |
| email: z | |
| .string() | |
| .nonempty("The email is required.") | |
| .email({ message: "The email is invalid." }), | |
| confirmEmail: z.string(), | |
| // interesting case : 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 | |
| .string() | |
| .or(z.null()) | |
| .refine((val) => Boolean(val), { | |
| message: "Please, make a choice!", | |
| }), | |
| //title: z.union([z.literal("Mr"), z.literal("Mrs"), z.literal("Miss"), z.literal("Dr")]), | |
| title: z.enum(["Mr", "Mrs", "Miss", "Dr"]), // For educationnal purpose (it's overkill here, as the UI constrains it already with a select). | |
| }) | |
| // 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 FormData = z.infer<typeof userSchema> | |
| /* ---------- React component ----------*/ | |
| export default function App() { | |
| const { | |
| register, | |
| handleSubmit, | |
| formState: { errors }, | |
| } = useForm<FormData>({ | |
| resolver: zodResolver(userSchema), // Configuration the validation with the zod schema. | |
| }) | |
| const [user, setUser] = React.useState<FormData>() | |
| // The onSubmit function is invoked by RHF only if the validation is OK. | |
| const onSubmit = (data) => { | |
| console.log("dans onSubmit", data) | |
| const user = userSchema.parse(data) // parse is not supposed to throw an error because RHF check the data first. | |
| console.log(user) | |
| setUser(user) | |
| } | |
| console.log("errors", errors) | |
| 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 }} | |
| > | |
| {/* use aria-invalid to indicate field contain error */} | |
| <input | |
| type="text" | |
| placeholder="First name is not mandatory" | |
| {...register("firstName")} | |
| aria-invalid={errors.firstName ? "true" : "false"} | |
| /> | |
| <AlertInput>{errors?.firstName?.message}</AlertInput> | |
| <input | |
| type="text" | |
| placeholder="Last name (mandatory)" | |
| {...register("lastName")} | |
| aria-invalid={errors.lastName ? "true" : "false"} | |
| /> | |
| <AlertInput>{errors?.lastName?.message}</AlertInput> | |
| <input | |
| type="text" | |
| placeholder="Email (mandatory)" | |
| {...register("email")} | |
| aria-invalid={errors.email ? "true" : "false"} | |
| /> | |
| <AlertInput>{errors?.email?.message}</AlertInput> | |
| <input | |
| type="text" | |
| placeholder="The same email as above" | |
| {...register("confirmEmail")} | |
| aria-invalid={errors.confirmEmail ? "true" : "false"} | |
| /> | |
| <AlertInput>{errors?.confirmEmail?.message}</AlertInput> | |
| <input | |
| type="tel" | |
| placeholder="Mobile number (mandatory)" | |
| {...register("mobileNumber")} | |
| aria-invalid={errors.mobileNumber ? "true" : "false"} | |
| /> | |
| <AlertInput>{errors?.mobileNumber?.message}</AlertInput> | |
| <select {...register("title")} aria-invalid={errors.title ? "true" : "false"}> | |
| <option value="Mr">Mr</option> | |
| <option value="Mrs">Mrs</option> | |
| <option value="Miss">Miss</option> | |
| <option value="Dr">Dr</option> | |
| </select> | |
| <div> | |
| <p>Are you a developer? (mandatory)</p> | |
| <input {...register("isDeveloper")} type="radio" value="Yes" /> Yes | |
| </div> | |
| <div> | |
| <input {...register("isDeveloper")} type="radio" value="No" /> No | |
| </div> | |
| <AlertInput>{errors?.isDeveloper?.message}</AlertInput> | |
| <input type="submit" /> | |
| </form> | |
| {Boolean(user) && ( | |
| <> | |
| <h1>Résultats</h1> | |
| <pre>{JSON.stringify(user, null, 2)}</pre> | |
| </> | |
| )} | |
| </> | |
| ) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment