跳到主要内容

Form Control with Zod, React Hook Form, Shadcn, and

Overview

  • Shadcn is a library of pre-built components that are styled with Tailwind CSS.
  • Zod is a schema declaration and validation library for TypeScript.
  • React Hook Form is a library for managing forms in React.

Antonomy of Components

An example of Form in Shadcn

import { useForm } from 'react-hook-form'
<Form>
const form = useForm()

<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>This is your public display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</Form>
  • Form is a wrapper component that provides the context for the form.
  • FormField is a component that renders a form field.
  • FormItem is a component that renders a form item.
  • FormLabel is a component that renders a form label.
  • FormControl is a component that renders a form control.
  • FormDescription is a component that renders a form description.
  • FormMessage is a component that renders a form message.

But we need more to cooperate with react-hook-form and zod.

An example of Form in Shadcn with react-hook-form and zod

1. Create a schema with zod.

"use client"

import { z } from "zod"

const formSchema = z.object({
username: z.string().min(2).max(50),
})
  • zod is a schema declaration and validation library for TypeScript.
  • The reason why we use "use client" is that zod is a client-side library.
  • z.object({}) is a function that creates a schema for an object.

2. Use the schema with react-hook-form and zod

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"

const formSchema = z.object({
username: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
})

export function ProfileForm() {
// 1. Define your form.
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
},
})

// 2. Define a submit handler.
function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
console.log(values)
}
}
  • zodResolver(formSchema) is a function that resolves the schema to a resolver.
  • This resolver is used to validate the form values.
  • useForm<z.infer<typeof formSchema>>({}) is a function that creates a form.
  • z.infer<typeof formSchema> is a function that infers the type of the form values.

3. Use the form in the UI

Here we need to use Form component from Shadcn and put validation logic in onSubmit function.

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"

import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"

const formSchema = z.object({
username: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
})

export function ProfileForm() {
//const form = useForm<z.infer<typeof formSchema>>({ ... })
// ...

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
  • <Form {..form}> is used to pass the form context to Form component. form comes from useForm hook.
  • onSubmit={form.handleSubmit(onSubmit)} is used to handle the form submission. And this is still on original form component.
  • <FormField> is used to render a form field.
  • In the FormField, control={form.control} is used to pass the form control to the field. from.control comes from useForm hook. Even though we didn't define control explicitly, useForm has already provided it by default.
  • Also, field in render={({ field }) => (...)} is used to bind the field to the input. It comes from controller of useController. And FormField is a wrapper of Controller component.
  • Remeber to spread the field to Input component.

ref: https://ui.shadcn.com/docs/components/form