React Hook Form
A guide to constructing robust, accessible, and type-safe forms in React using React Hook Form for state management and Zod for schema validation, integrated with our UI components.
Forms represent a critical interface in web applications, yet they often introduce significant complexity due to state management, validation, accessibility, and styling requirements.
Effective HTML forms should ideally possess the following characteristics:
Semantic Structure: Utilize appropriate HTML elements (
<form>,<label>,<input>, etc.) for inherent meaning and browser/assistive technology understanding.Navigability: Ensure seamless keyboard navigation between fields using Tab and Shift+Tab.
Accessibility: Implement ARIA attributes where necessary and associate labels correctly with their corresponding inputs for screen reader compatibility.
Robust Validation: Include mechanisms for both client-side (immediate feedback) and server-side (authoritative) validation.
Consistent Styling: Maintain visual harmony with the overall application design system.
This guide focuses on leveraging the strengths of react-hook-form for efficient form state management and performance, combined with zod for powerful schema definition and validation. We introduce a set of composable <Form> components, including <FormField>, designed to bridge these libraries with our accessible Radix UI-based input components, simplifying the creation of high-quality forms.
Key Features of this Form Approach
Our <Form> component set acts as a helpful layer on top of react-hook-form, streamlining the integration with our UI library and Zod. It offers:
Composable Structure: Provides components like
<Form>,<FormField>,<FormItem>,<FormLabel>,<FormControl>,<FormDescription>, and<FormMessage>for building forms declaratively.Simplified Field Control: The
<FormField>component abstracts the connection betweenreact-hook-form's state and controlled input components.Integrated Zod Validation: Seamlessly integrates Zod schemas for robust type-safe validation via the
@hookform/resolverslibrary.Enhanced Accessibility: Automatically handles ARIA attributes (like
aria-describedby,aria-invalid) and associates labels with inputs usingReact.useId()for unique, stable IDs. Error messages are linked correctly.Radix UI Compatibility: Designed to work smoothly with all Radix UI-based input components (Input, Select, Checkbox, etc.).
Schema Agnostic (BYOS): While Zod is used in examples, the underlying structure supports other schema validation libraries compatible with
react-hook-formresolvers.Full Markup Control: You retain complete control over the rendered HTML structure and styling, applying Tailwind CSS classes or custom styles as needed.
Form Component Anatomy
The core structure revolves around the <FormField> component, which connects a specific field in your react-hook-form state to the UI input elements.
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="fieldName"
render={({ field }) => (
<FormItem>
<FormLabel>Field Label</FormLabel>
<FormControl>
<Input placeholder="Enter value..." {...field} />
</FormControl>
<FormDescription>Provide additional context here.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* ... more FormField components ... */}
<Button type="submit">Submit</Button>
</form>
</Form>This structure ensures proper accessibility linkages (label to input, input to description/error message) and provides clear containers for styling individual parts of a form field.
Basic Field Example
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 Component Installation
Command
npx shadcn@latest add formStep-by-Step Form Implementation
Step 1: Define Your Form Schema with Zod
Begin by defining the structure and validation rules for your form data using a Zod schema. Zod provides a powerful and type-safe way to declare expected data types, required fields, string lengths, email formats, and more. Consult the Zod documentation for detailed schema creation options.
"use client";
const formSchema = z.object({
username: z.string().min(2).max(50),
});Step 2: Initialize the Form with useForm
In your React component, utilize the useForm hook provided by react-hook-form to initialize your form's state management. Configure it with your Zod schema using the zodResolver and provide defaultValues for your form fields.
"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);
}
}Since FormField is using a controlled component, you need to provide a default value for the field. See the React Hook Form docs to learn more about controlled components.
Step 3: Construct the Form UI
With the form state initialized, use the <Form> and <FormField> components to render the actual form elements. Connect the form object from useForm to the <Form> component and pass form.control to each <FormField>. Use the render prop pattern within <FormField> to structure your label, input, description, and error message using the provided sub-components.
"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() {
// ...
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>
);
}Step 4: Completion
Following these steps results in a fully functional, accessible, and type-safe form with integrated client-side validation powered by Zod and React Hook Form, rendered using our consistent UI components.
Further Form Integration Examples
The <Form> component system is designed to work seamlessly with various input components. Explore these examples to see specific integrations: