V0 generates form inputs that lose validation state because it defaults to uncontrolled components and omits react-hook-form registration. Fix this by adding the "use client" directive, switching to controlled inputs with proper onChange handlers, and wiring up a validation library like react-hook-form or zod to catch errors before submission.
Why V0 form inputs fail validation
V0 generates visually correct form layouts using shadcn/ui Input and Label components, but it frequently skips the wiring that makes validation actually work. The AI tends to create uncontrolled inputs without onChange handlers, forgets to add the "use client" directive (which means event handlers silently do nothing in a Server Component), and generates onSubmit functions that read from state variables that were never updated. When you combine these gaps with the fact that V0 sometimes imports a Form component from shadcn that wraps react-hook-form but never calls register or control, you get forms that look perfect but accept any input or reject everything.
- Missing "use client" directive causes onClick and onChange handlers to be ignored in Server Components
- V0 generates uncontrolled inputs without connecting them to React state or a form library
- shadcn/ui Form component is generated but react-hook-form register or control props are not passed to inputs
- Validation schema is defined with zod but never connected to the form resolver
- V0 sometimes generates duplicate name attributes or omits them entirely, breaking FormData collection
Error messages you might see
TypeError: Cannot read properties of undefined (reading 'onChange')The shadcn FormField component expects a react-hook-form control prop, but V0 generated the form without initializing useForm or passing control to the FormField.
Warning: A component is changing an uncontrolled input to be controlledV0 initialized the input without a value prop (uncontrolled), then later sets state that provides a value prop. Initialize state with an empty string instead of undefined.
Error: Objects are not valid as a React child (found: object with keys {message})V0 tried to render the zod error object directly in JSX instead of accessing the error message string property.
Before you start
- A V0 project with a form that is not validating correctly
- Basic understanding of React controlled components and form state
- shadcn/ui form components installed in your project (Input, Label, Button)
How to fix it
Add the "use client" directive to your form component
In Next.js App Router, components are Server Components by default. Event handlers like onChange and onSubmit only work in Client Components. Without this directive, your form renders but no interaction works.
Add the "use client" directive to your form component
In Next.js App Router, components are Server Components by default. Event handlers like onChange and onSubmit only work in Client Components. Without this directive, your form renders but no interaction works.
Open your form component file and add "use client" as the very first line, before any imports. This tells Next.js to render the component on the client where DOM events are available.
import { Input } from "@/components/ui/input"import { Button } from "@/components/ui/button"export default function ContactForm() { const handleSubmit = (e: React.FormEvent) => { e.preventDefault() // This never fires without "use client" } return <form onSubmit={handleSubmit}>...</form>}"use client"import { Input } from "@/components/ui/input"import { Button } from "@/components/ui/button"export default function ContactForm() { const handleSubmit = (e: React.FormEvent) => { e.preventDefault() // Now fires correctly } return <form onSubmit={handleSubmit}>...</form>}Expected result: The form's onSubmit handler fires when the user clicks the submit button. You can verify by adding a console.log inside the handler.
Switch to controlled inputs with useState
V0 often generates inputs without value and onChange props, meaning React has no idea what the user typed. Controlled inputs keep the component state in sync with what the user sees.
Switch to controlled inputs with useState
V0 often generates inputs without value and onChange props, meaning React has no idea what the user typed. Controlled inputs keep the component state in sync with what the user sees.
For each input field, create a state variable with useState, bind it to the value prop, and update it via onChange. Initialize each state variable with an empty string to avoid the uncontrolled-to-controlled warning.
<Input type="email" placeholder="you@example.com" className="w-full"/>const [email, setEmail] = useState("")<Input type="email" placeholder="you@example.com" className="w-full" value={email} onChange={(e) => setEmail(e.target.value)}/>Expected result: Typing in the email field updates the email state variable. You can verify by adding a temporary display of the value below the input.
Wire up react-hook-form with zod validation
Manual validation with useState leads to verbose, error-prone code. react-hook-form with zod provides type-safe schema validation, automatic error messages, and proper form state management that integrates cleanly with shadcn/ui Form components.
Wire up react-hook-form with zod validation
Manual validation with useState leads to verbose, error-prone code. react-hook-form with zod provides type-safe schema validation, automatic error messages, and proper form state management that integrates cleanly with shadcn/ui Form components.
Install react-hook-form and @hookform/resolvers if not already present. Define a zod schema for your form, create a form instance with useForm and the zodResolver, then connect each field using the FormField component from shadcn/ui.
export default function ContactForm() { const [name, setName] = useState("") const [email, setEmail] = useState("") const handleSubmit = () => { if (!name) alert("Name required") if (!email) alert("Email required") }}import { useForm } from "react-hook-form"import { zodResolver } from "@hookform/resolvers/zod"import * as z from "zod"const formSchema = z.object({ name: z.string().min(2, "Name must be at least 2 characters"), email: z.string().email("Please enter a valid email address"),})export default function ContactForm() { const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), defaultValues: { name: "", email: "" }, }) function onSubmit(values: z.infer<typeof formSchema>) { console.log(values) }}Expected result: Submitting the form with empty fields shows inline error messages beneath each input. Submitting with valid data logs the form values to the console.
Connect FormField components to display validation errors
The validation schema catches errors, but the user never sees them unless FormMessage is rendered inside each FormField. V0 sometimes generates FormField without the FormMessage child.
Connect FormField components to display validation errors
The validation schema catches errors, but the user never sees them unless FormMessage is rendered inside each FormField. V0 sometimes generates FormField without the FormMessage child.
Wrap each input in a FormField from shadcn/ui that receives the form control prop. Include FormLabel, FormControl wrapping the Input, FormDescription for help text, and FormMessage which automatically renders any validation errors for that field.
<form> <label>Name</label> <Input name="name" /> <label>Email</label> <Input name="email" /> <Button type="submit">Submit</Button></form><Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <FormField control={form.control} name="name" render={({ field }) => ( <FormItem> <FormLabel>Name</FormLabel> <FormControl> <Input placeholder="John Doe" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <Button type="submit">Submit</Button> </form></Form>Expected result: Each form field shows a red error message directly beneath it when validation fails. The error messages match what you defined in the zod schema.
Complete code example
1"use client"23import { useForm } from "react-hook-form"4import { zodResolver } from "@hookform/resolvers/zod"5import * as z from "zod"6import { Button } from "@/components/ui/button"7import { Input } from "@/components/ui/input"8import {9 Form,10 FormControl,11 FormField,12 FormItem,13 FormLabel,14 FormMessage,15} from "@/components/ui/form"1617const formSchema = z.object({18 name: z.string().min(2, "Name must be at least 2 characters"),19 email: z.string().email("Please enter a valid email address"),20 message: z.string().min(10, "Message must be at least 10 characters"),21})2223export default function ContactPage() {24 const form = useForm<z.infer<typeof formSchema>>({25 resolver: zodResolver(formSchema),26 defaultValues: { name: "", email: "", message: "" },27 })2829 function onSubmit(values: z.infer<typeof formSchema>) {30 console.log("Form submitted:", values)31 }3233 return (34 <div className="max-w-md mx-auto p-6">35 <h1 className="text-2xl font-bold mb-6">Contact Us</h1>36 <Form {...form}>37 <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">38 <FormField39 control={form.control}40 name="name"41 render={({ field }) => (42 <FormItem>43 <FormLabel>Name</FormLabel>44 <FormControl>45 <Input placeholder="John Doe" {...field} />46 </FormControl>47 <FormMessage />48 </FormItem>49 )}50 />51 <FormField52 control={form.control}53 name="email"54 render={({ field }) => (55 <FormItem>56 <FormLabel>Email</FormLabel>57 <FormControl>58 <Input placeholder="you@example.com" {...field} />59 </FormControl>60 <FormMessage />61 </FormItem>62 )}63 />64 <FormField65 control={form.control}66 name="message"67 render={({ field }) => (68 <FormItem>69 <FormLabel>Message</FormLabel>70 <FormControl>71 <Input placeholder="Your message..." {...field} />72 </FormControl>73 <FormMessage />74 </FormItem>75 )}76 />77 <Button type="submit" className="w-full">78 Send Message79 </Button>80 </form>81 </Form>82 </div>83 )84}Best practices to prevent this
- Always add "use client" to any component that uses event handlers, useState, or useEffect in Next.js App Router
- Initialize all form state with empty strings rather than undefined to prevent uncontrolled-to-controlled warnings
- Use react-hook-form with zodResolver for type-safe validation instead of manual if-else checks
- Include FormMessage inside every FormField so users see exactly which field has an error
- Set defaultValues in useForm for every field to avoid undefined initial render values
- Test form validation with empty submissions, boundary values, and valid data before deploying
- Keep validation schemas in a separate file when they are shared across multiple forms
- Use form.formState.isSubmitting to disable the submit button and prevent double submissions
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Next.js App Router form generated by V0 that is not validating inputs. The form uses shadcn/ui components. Help me wire up react-hook-form with zod validation so each field shows inline error messages. Include the "use client" directive and FormField/FormMessage components.
Frequently asked questions
Why does my V0 form submit without showing any validation errors?
The most common cause is a missing "use client" directive. In Next.js App Router, components are Server Components by default, and event handlers like onSubmit silently do nothing. Add "use client" as the first line of your form component file.
How do I add inline validation errors to V0 form inputs?
Use the shadcn/ui Form components with react-hook-form. Wrap each input in a FormField component, include a FormMessage child which automatically displays validation errors, and connect a zod schema via zodResolver in useForm.
Why do I get a warning about changing an uncontrolled input to controlled?
V0 sometimes initializes state as undefined rather than an empty string. When the input first renders without a value prop and then receives one, React warns about the switch. Set defaultValues with empty strings in useForm or initialize useState with an empty string.
Can I use V0 to generate react-hook-form code directly?
Yes. Prompt V0 with specifics like: "Create a registration form using react-hook-form, zod validation, and shadcn/ui Form components with inline error messages." V0 handles this well when explicitly asked, but often skips validation when the prompt just says "create a form."
What if my V0 form has complex validation like password confirmation?
Use zod's refine method to add cross-field validation. For example, z.object({ password: z.string(), confirm: z.string() }).refine(data => data.password === data.confirm, { message: "Passwords must match", path: ["confirm"] }). This works seamlessly with react-hook-form and shadcn FormMessage.
Should I use RapidDev for complex multi-step form validation in V0?
If your form spans multiple steps with conditional validation, file uploads, and API integration, the complexity can quickly exceed what V0 generates reliably. RapidDev engineers can build production-grade multi-step forms with proper error handling and state persistence across steps.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your issue.
Book a free consultation