How to Implement Form Validation Using Zod with the Field Component in refine-shadcn
Use zodResolver from @hookform/resolvers/zod to connect your Zod schema to useForm from @refinedev/react-hook-form, then spread the returned form object onto the Field component to automatically validate inputs and display error messages.
The refine-shadcn library (available at ferdiunal/refine-shadcn) provides a streamlined approach to form validation by integrating Zod schemas with a custom Field component. This architecture leverages react-hook-form under the hood while exposing a declarative API that works seamlessly with Shadcn UI components to handle complex validation logic.
Understanding the Validation Flow
The implementation follows a five-step architecture that bridges Zod validation with UI rendering:
- Schema Definition – You define validation rules using
z.object()in a Zod schema, specifying constraints like.min(),.email(), or.max()with custom error messages. - Resolver Integration – The
zodResolveradapter (from@hookform/resolvers/zod) translates Zod validation errors into the format expected byreact-hook-form. - Form State Initialization –
useFormfrom@refinedev/react-hook-formreceives the resolver and creates the form control object containingcontrol,handleSubmit, andformState. - Field Registration – The
Fieldcomponent (implemented inpackages/theme/src/components/field.tsx) receives the form context via spread props and registers individual inputs usingreact-hook-form'sController. - Error Rendering – The
Fieldcomponent automatically rendersFormMessage(frompackages/theme/src/ui/form.tsx) which pulls validation errors fromformState.errorsand displays them with appropriate ARIA attributes.
Step 1: Define Your Zod Schema
Create a Zod schema that describes the shape of your form data and validation requirements. This schema enforces type safety and provides detailed error messages for each validation rule.
import * as z from "zod";
const userSchema = z.object({
firstName: z.string().min(2, {
message: "Firstname must be at least 2 characters."
}),
lastName: z.string().min(2, {
message: "Lastname must be at least 2 characters."
}),
email: z.string().email({
message: "Please enter a valid email address."
}),
});
Step 2: Connect Zod to react-hook-form
Initialize your form using useForm from @refinedev/react-hook-form and pass the zodResolver adapter with your schema. The mode: "all" configuration ensures validation runs on both change and blur events.
import { useForm } from "@refinedev/react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const UserForm = () => {
const form = useForm<z.infer<typeof userSchema>>({
mode: "all",
resolver: zodResolver(userSchema),
defaultValues: {
firstName: "",
lastName: "",
email: "",
},
});
const onSubmit = (data: z.infer<typeof userSchema>) => {
console.log(data);
};
return (
// Form JSX will be implemented in the next step
<form onSubmit={form.handleSubmit(onSubmit)} />
);
};
Step 3: Implement the Field Component
The Field component from @ferdiunal/refine-shadcn abstracts the complexity of react-hook-form's Controller. It accepts the form context via spread props and uses a render prop pattern to pass field handlers to your input components.
import { Field, Form } from "@ferdiunal/refine-shadcn";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Field {...form} name="firstName" label="Firstname">
{(field) => <Input {...field} placeholder="Firstname" />}
</Field>
<Field {...form} name="lastName" label="Lastname">
{(field) => <Input {...field} placeholder="Lastname" />}
</Field>
<Field {...form} name="email" label="Email">
{(field) => <Input {...field} placeholder="Email" type="email" />}
</Field>
<Button type="submit">Submit</Button>
</form>
</Form>
The Field component automatically handles:
- Label rendering via the
labelprop - Error message display through
FormMessage, which reads fromformState.errors - Description text (optional) for helper text below inputs
- ARIA attributes for accessibility, managed by the underlying
FormFieldimplementation
Key Source Files
Understanding the underlying implementation helps when customizing validation behavior or troubleshooting issues:
-
packages/theme/src/components/field.tsx– Implements theFieldcomponent, wrappingreact-hook-form'sControllerand renderingFormLabel,FormControl, andFormMessage. -
packages/theme/src/components/form.tsx– Provides theFormwrapper component that injects theFormProvidercontext, making form state available to all child components. -
packages/theme/src/ui/form.tsx– Houses the low-level UI primitives (FormItem,FormLabel,FormMessage) that handle styling, error display, and ARIA attributes. -
templates/vite-react/src/pages/users/Form.tsx– Production-ready reference implementation demonstrating Zod validation with theFieldcomponent in a real-world user management form.
Summary
-
Zod schemas define validation rules and custom error messages using methods like
.min(),.email(), and.max(). -
zodResolverbridges Zod withreact-hook-form, translating schema validation errors into the format expected by the form state manager. -
The
Fieldcomponent from@ferdiunal/refine-shadcnabstracts thereact-hook-formController, automatically wiring up validation, labels, and error messages through a render prop pattern. -
Error display happens automatically via
FormMessageinside theFieldcomponent, which reads fromformState.errorsand renders appropriate feedback without additional configuration. -
Source files in
packages/theme/src/components/provide the implementation details for customization, whiletemplates/vite-react/src/pages/users/Form.tsxserves as the canonical usage example.
Frequently Asked Questions
Can I use validation libraries other than Zod with the Field component?
Yes. The Field component accepts any resolver compatible with react-hook-form, including Yup, Joi, or custom validators. Simply replace zodResolver with your preferred resolver (such as yupResolver from @hookform/resolvers/yup) and ensure your validation schema matches the expected data structure. The Field component will automatically display errors regardless of which resolver generates them.
How do I display custom error messages for specific validation rules?
Define custom messages directly in your Zod schema using the .min(), .max(), .email(), or .refine() methods with a message property. The Field component automatically passes these messages to FormMessage, which renders them when validation fails. For example: z.string().min(5, { message: "Title must be at least 5 characters" }). You can also override messages globally by customizing the FormMessage component in packages/theme/src/ui/form.tsx.
Does the Field component support nested form values or array fields?
Yes. Use dot notation for nested objects (e.g., name="profile.bio") or bracket notation for arrays (e.g., name="tags[0]"). The Field component registers these paths with react-hook-form's Controller, and Zod validates them according to your schema definition using z.object() for nested shapes or z.array() for collections. Ensure your defaultValues structure matches the nested path structure to avoid undefined errors.
How do I customize the styling of validation error messages?
The error messages render through the FormMessage component located in packages/theme/src/ui/form.tsx. You can customize the appearance by modifying the FormMessage implementation to use different Tailwind classes or by wrapping the Field component and intercepting formState.errors to render custom error UI. The default implementation uses standard Shadcn UI styling with destructive color variants (text-destructive) for error states and automatically manages ARIA attributes for accessibility.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →