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

使用ZodResolver时React Hook Form保存按钮禁用且无法提交的问题

问题解决:React Hook Form + Zod 表单验证导致保存按钮禁用

核心问题分析及修复步骤


1. 移除useForm多余的依赖数组

你的useForm调用多传了一个空数组[],React Hook Form的useForm只接受一个配置对象参数,多余的数组会导致表单初始化异常,状态更新逻辑出错。

修改前:

const form = useForm({
    resolver: zodResolver(priceSchema),
    defaultValues: course
}, []);

修改后:

const form = useForm({
    resolver: zodResolver(priceSchema),
    defaultValues: course
});

2. 解决输入值类型不匹配问题

HTML的number输入框实际返回的是字符串类型,但你的Zod schema要求price是number类型,这会导致验证一直不通过,isValid始终为false。

需要在register里添加valueAsNumber转换,确保输入值被转为数字:

修改input的register部分:

<input
    className='p-1 shadow-lg appearance-none rounded w-full outline-none'
    type="number"
    id='price'
    placeholder="Enter course selling price..."
    disabled={isSubmitting}
    {...form.register('price', { valueAsNumber: true })}
/>

注意:移除input上的defaultValue,因为useForm的defaultValues已经会自动设置初始值,重复设置会导致状态不一致。


3. 修正表单提交按钮的位置和绑定

你的保存按钮不在<form>标签内部,且用了onClick绑定handleSubmit,这不符合表单提交的标准逻辑,容易导致验证流程失效。

把编辑状态下的按钮组移到<form>内部,直接利用form的onSubmit事件:

修改后的JSX结构:

{isEditing ? (
    <form className='mt-2' onSubmit={handleSubmit(onSubmit)}>
        <input
            className='p-1 shadow-lg appearance-none rounded w-full outline-none'
            type="number"
            id='price'
            placeholder="Enter course selling price..."
            disabled={isSubmitting}
            {...form.register('price', { valueAsNumber: true })}
        />
        {/* 把按钮移到form内部 */}
        <div className='flex gap-x-2 mt-2'>
            <button
                className='px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-md'
                type="button"
                onClick={() => {
                    setIsEditing(false)
                    reset(course)
                }}>
                Cancel
            </button>
            <button
                type='submit'
                disabled={!isValid || isSubmitting}
                className='px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-md'>
                Save
            </button>
        </div>
    </form>
) : (
    <p className='text-md font-semibold mt-2 py-1'>
        {course.price || 0}
    </p>
)}

同时移除原来在form外的按钮组,确保submit按钮在form内,type为submit,不需要额外绑定onClick。


4. 验证初始值的类型

确保course.price是数字类型,如果后端返回的是字符串,需要在设置defaultValues时转换:

const form = useForm({
    resolver: zodResolver(priceSchema),
    defaultValues: {
        ...course,
        price: Number(course.price) || 0
    }
});

完整修复后的代码片段

import React, { useState } from 'react'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'

const priceSchema = z.object({
    price: z.number().positive().int()
})

const PriceForm = ({ course, courseId, fetchData }) => {
    const [isEditing, setIsEditing] = useState(false);
    // 修复useForm的参数,转换初始值类型
    const form = useForm({
        resolver: zodResolver(priceSchema),
        defaultValues: {
            ...course,
            price: Number(course.price) || 0
        }
    });

    const { handleSubmit, reset } = form;
    const { isSubmitting, isValid } = form.formState;

    const onSubmit = async (values) => {
        try {
            setIsEditing(false);
            await fetch(`http://localhost:3000/instructor/courses/${courseId}`, {
                method: 'PATCH',
                headers: {
                    authorization: `Bearer ${localStorage.getItem('token')}`,
                    'content-type': 'application/json'
                },
                body: JSON.stringify(values)
            });
            fetchData();
        } catch (error) {
            console.log(error);
        }
    }

    return (
        <div className='mt-6 border bg-slate-200 rounded-md p-4'>
            <div className='font-medium flex items-center justify-between'>
                <p className='text-zinc-600 py-2'>Course Price</p>
                {!isEditing && (
                    <button onClick={() => setIsEditing(true)}>
                        🖋 Edit
                    </button>
                )}
            </div>

            {isEditing ? (
                <form className='mt-2' onSubmit={handleSubmit(onSubmit)}>
                    <input
                        className='p-1 shadow-lg appearance-none rounded w-full outline-none'
                        type="number"
                        id='price'
                        placeholder="Enter course selling price..."
                        disabled={isSubmitting}
                        {...form.register('price', { valueAsNumber: true })}
                    />
                    <div className='flex gap-x-2 mt-2'>
                        <button
                            className='px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-md'
                            type="button"
                            onClick={() => {
                                setIsEditing(false)
                                reset({
                                    ...course,
                                    price: Number(course.price) || 0
                                })
                            }}>
                            Cancel
                        </button>
                        <button
                            type='submit'
                            disabled={!isValid || isSubmitting}
                            className='px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-md'>
                            Save
                        </button>
                    </div>
                </form>
            ) : (
                <p className='text-md font-semibold mt-2 py-1'>
                    {course.price || 0}
                </p>
            )}
        </div>
    )
}

export default PriceForm

内容的提问来源于stack exchange,提问作者Prateek Kumar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 06:27:34