如何在动态数据表单中使用shadcn受控Input并解决Zod验证问题
问题描述
在Next.js 13项目中使用shadcn的Form组件,需要实现两个功能:
- 输入框动态加载待编辑的目标内容
- 点击“添加”按钮后输入新值
目前功能可正常运行,但Zod验证无法识别表单值,输入框下方始终显示“Required”错误提示。请问在shadcn Input组件中使用value和onChange属性时,如何让Zod感知到表单值?
代码示例
'use client'; import React, { useEffect } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; import { Button } from '@/components/ui/button'; import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { toast } from '@/components/ui/use-toast'; import ButtonLoading from './ui/ButtonLoading'; import { useState } from 'react'; import { GoalType } from '@/types/goal'; interface GoalDayFormProps { editedGoal: GoalType | null; setEditedGoal: React.Dispatch<React.SetStateAction<GoalType | null>>; } const GoalDayForm = ({ editedGoal, setEditedGoal }: GoalDayFormProps) => { const [loading, setLoading] = useState(false); const [addingGoal, setAddingGoal] = useState(false); const [newGoalText, setNewGoalText] = useState(''); useEffect(() => { if (editedGoal) { setAddingGoal(false); } }, [editedGoal]); const FormSchema = z.object({ goal: z.string().min(1, { message: 'The goal field cannot be empty.', }), }); const form = useForm<z.infer<typeof FormSchema>>({ resolver: zodResolver(FormSchema), }); const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (editedGoal) { setEditedGoal((prev: any) => ({ ...prev, text: e.target.value })); } setNewGoalText(e.target.value); }; function onSubmit(data: z.infer<typeof FormSchema>) { console.log(data); toast({ title: 'You submitted the following values:', description: ( <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4"> <code className="text-white">{JSON.stringify(data, null, 2)}</code> </pre> ), }); if (editedGoal) { setEditedGoal(null); } } return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="mt-[1.5rem] w-full space-y-4"> {(addingGoal || editedGoal) && ( <FormField control={form.control} name="goal" render={({ field }) => ( <FormItem> <FormControl> <Input placeholder="Goal description" {...field} value={editedGoal ? editedGoal.text : newGoalText} onChange={(e) => handleInputChange(e)} /> </FormControl> <FormMessage /> </FormItem> )} /> )} <div className="flex gap-4"> {addingGoal && ( <> <Button variant="ghost" disabled={loading} onClick={() => setAddingGoal(false)} > Cancel </Button> <ButtonLoading loading={loading} type="submit"> Add goal </ButtonLoading> </> )} {editedGoal && ( <> <Button variant="ghost" disabled={loading} onClick={() => setEditedGoal(null)} > Cancel </Button> <ButtonLoading loading={loading} type="submit"> Edit goal </ButtonLoading> </> )} {!addingGoal && !editedGoal && ( <Button onClick={() => setAddingGoal(true)}>New Goal</Button> )} </div> </form> </Form> ); }; export default GoalDayForm;
解决方案
问题根源在于你同时维护了自定义状态(newGoalText、editedGoal.text)和react-hook-form的表单状态,但没有将前者同步到后者。Zod验证依赖的是react-hook-form内部的状态,而你UI显示的是自定义状态,两者不一致导致验证错误。
修复步骤:
- 移除自定义状态
newGoalText:完全依靠react-hook-form管理表单值,无需额外维护输入框状态。 - 同步编辑状态到表单:当
editedGoal变化时,用react-hook-form的setValue方法更新表单的goal字段,确保内部状态与UI显示一致。 - 简化输入框的value和onChange处理:直接使用FormField提供的
field属性,或者在需要同步editedGoal时,同时更新表单状态。
修改后的代码:
'use client'; import React, { useEffect } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; import { Button } from '@/components/ui/button'; import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { toast } from '@/components/ui/use-toast'; import ButtonLoading from './ui/ButtonLoading'; import { useState } from 'react'; import { GoalType } from '@/types/goal'; interface GoalDayFormProps { editedGoal: GoalType | null; setEditedGoal: React.Dispatch<React.SetStateAction<GoalType | null>>; } const GoalDayForm = ({ editedGoal, setEditedGoal }: GoalDayFormProps) => { const [loading, setLoading] = useState(false); const [addingGoal, setAddingGoal] = useState(false); const FormSchema = z.object({ goal: z.string().min(1, { message: 'The goal field cannot be empty.', }), }); const form = useForm<z.infer<typeof FormSchema>>({ resolver: zodResolver(FormSchema), defaultValues: { goal: '', }, }); // 当编辑目标变化时,同步到表单 useEffect(() => { if (editedGoal) { setAddingGoal(false); form.setValue('goal', editedGoal.text); } else if (!addingGoal) { form.reset(); } }, [editedGoal, addingGoal, form]); const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { // 更新表单状态 form.setValue('goal', e.target.value); // 如果是编辑状态,同步更新editedGoal if (editedGoal) { setEditedGoal(prev => prev ? { ...prev, text: e.target.value } : null); } }; function onSubmit(data: z.infer<typeof FormSchema>) { console.log(data); toast({ title: 'You submitted the following values:', description: ( <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4"> <code className="text-white">{JSON.stringify(data, null, 2)}</code> </pre> ), }); if (editedGoal) { setEditedGoal(null); } else { setAddingGoal(false); form.reset(); } } return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="mt-[1.5rem] w-full space-y-4"> {(addingGoal || editedGoal) && ( <FormField control={form.control} name="goal" render={({ field }) => ( <FormItem> <FormControl> <Input placeholder="Goal description" {...field} onChange={handleInputChange} /> </FormControl> <FormMessage /> </FormItem> )} /> )} <div className="flex gap-4"> {addingGoal && ( <> <Button variant="ghost" disabled={loading} onClick={() => { setAddingGoal(false); form.reset(); }} > Cancel </Button> <ButtonLoading loading={loading} type="submit"> Add goal </ButtonLoading> </> )} {editedGoal && ( <> <Button variant="ghost" disabled={loading} onClick={() => setEditedGoal(null)} > Cancel </Button> <ButtonLoading loading={loading} type="submit"> Edit goal </ButtonLoading> </> )} {!addingGoal && !editedGoal && ( <Button onClick={() => setAddingGoal(true)}>New Goal</Button> )} </div> </form> </Form> ); }; export default GoalDayForm;
关键改动说明:
- 移除了
newGoalText状态,表单值完全由react-hook-form管理 - 添加
useEffect监听editedGoal变化,调用form.setValue将编辑内容同步到表单 - 修改
handleInputChange,先更新表单状态,再同步到editedGoal(如果是编辑模式) - 取消按钮点击时重置表单状态,确保下次打开时输入框为空
这样Zod验证就能正确读取表单内部的状态,不会再出现错误提示,同时编辑和新增功能也能正常工作。
内容的提问来源于stack exchange,提问作者0mppu
相关产品推荐
相关产品推荐

