Skip to content

Instantly share code, notes, and snippets.

@Liopun
Forked from pom421/form-next-ts-rhf-zod-demo.tsx
Created December 20, 2022 21:30
Show Gist options
  • Select an option

  • Save Liopun/f5c9efad7eaa35c710421dd5d658629a to your computer and use it in GitHub Desktop.

Select an option

Save Liopun/f5c9efad7eaa35c710421dd5d658629a to your computer and use it in GitHub Desktop.
Form with React Hook form and zod rules (Next.js page example)
// 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