You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在动态数据表单中使用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显示的是自定义状态,两者不一致导致验证错误。

修复步骤:

  1. 移除自定义状态newGoalText:完全依靠react-hook-form管理表单值,无需额外维护输入框状态。
  2. 同步编辑状态到表单:当editedGoal变化时,用react-hook-form的setValue方法更新表单的goal字段,确保内部状态与UI显示一致。
  3. 简化输入框的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 23:24:57