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>
- Formis a wrapper component that provides the context for the form.
- FormFieldis a component that renders a form field.
- FormItemis a component that renders a form item.
- FormLabelis a component that renders a form label.
- FormControlis a component that renders a form control.
- FormDescriptionis a component that renders a form description.
- FormMessageis 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),
    })
- zodis a schema declaration and validation library for TypeScript.
- The reason why we use "use client"is thatzodis 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- Formcomponent.- formcomes from- useFormhook.
- onSubmit={form.handleSubmit(onSubmit)}is used to handle the form submission. And this is still on original- formcomponent.
- <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.controlcomes fromuseFormhook. Even though we didn't definecontrolexplicitly, useForm has already provided it by default.
- Also, fieldinrender={({ field }) => (...)}is used to bind the field to the input. It comes fromcontrollerofuseController. AndFormFieldis a wrapper ofControllercomponent.
- Remeber to spread the fieldtoInputcomponent.