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

React Hook Form结合Zod实现可选文件输入的验证问题

解决React Hook Form + Zod 可选文件上传验证问题

你的问题核心在于:未选择文件时,react-hook-form会给image字段传入空的FileList对象(而非undefined),但你当前的imageSchema里第一个refine规则强制要求files?.length >= 1,即使加了.optional()也会触发这个验证——因为.optional()只允许字段为undefined,不允许空的FileList。

要实现「仅在用户选择文件时才触发验证」,你需要调整Zod schema的逻辑,让验证规则仅在文件真正被选中时执行。以下是修改方案:

修改后的Schema代码

const imageSchema = z.union([
  // 允许字段为undefined(未选择文件的情况)
  z.undefined(),
  // 当有文件时,执行所有验证规则
  z.instanceof(FileList).refine((files) => files.length >= 1, { 
    message: 'Photo is required.' 
  }).refine((files) => ACCEPTED_IMAGE_TYPES.includes(files[0].type), {
    message: '.jpg, .jpeg, .png and .webp files are accepted.',
  }).refine((files) => files[0].size <= MAX_FILE_SIZE, {
    message: `Max file size is 2MB.`,
  })
])

const baseSchema = z.object({
  title: z.string().min(1, 'Title required').max(60, 'Maximum 60 characters.'),
  message: z.string().min(1, 'Message required.').max(120, 'Maximum 120 characters.'),
  image: imageSchema,
});

关键修改说明

  1. 用z.union区分两种状态:明确允许字段为undefined(对应未选择文件),同时对选中文件的情况(FileList实例)执行完整验证。
  2. 移除.optional():因为z.union已经包含了undefined的可选情况,不需要额外加.optional()。
  3. 直接使用z.instanceof(FileList):更精准地匹配文件输入的类型,避免z.any()的模糊性。

完整修改后的组件代码

'use client'

import { z } from "zod";
import { useForm } from 'react-hook-form'
import { zodResolver } from "@hookform/resolvers/zod";

export default function Form() {

    const MAX_FILE_SIZE = 2000000
    const ACCEPTED_IMAGE_TYPES = [
        'image/jpeg',
        'image/jpg',
        'image/png',
        'image/webp',
    ]

    const imageSchema = z.union([
        z.undefined(),
        z.instanceof(FileList)
            .refine((files) => files.length >= 1, { message: 'Photo is required.' })
            .refine((files) => ACCEPTED_IMAGE_TYPES.includes(files[0].type), {
                message: '.jpg, .jpeg, .png and .webp files are accepted.',
            })
            .refine((files) => files[0].size <= MAX_FILE_SIZE, {
                message: `Max file size is 2MB.`,
            })
    ])

    const baseSchema = z.object({
        title: z.string().min(1, 'Title required').max(60, 'Maximum 60 characters.'),
        message: z.string().min(1, 'Message required.').max(120, 'Maximum 120 characters.'),
        image: imageSchema,
    });


    type FormValues = z.infer<typeof baseSchema>;
    const { register, handleSubmit, control, watch, reset, formState: { errors, isSubmitting } } = useForm<FormValues>({
        resolver: zodResolver(baseSchema),
    });


    //submit form
    const onSubmit = async (data: any) => {
        console.log(data);
    }

    return (
        <div className="mx-auto max-w-md container">
            <form onSubmit={handleSubmit(onSubmit)}>
                <div className="py-8 p-10 bg-slate-100 border-slate-200 rounded-xl border-2">

                    <div className="mb-6">
                        <label className="text-gray-700 font-bold" htmlFor="name">Title</label>
                        <input type="text" {...register('title')} className="w-full border border-gray-300 py-2 pl-3 rounded mt-2 outline-none" />
                        {errors.title && <div className="text-red-500">{errors.title?.message}</div>}
                    </div>

                    <div className="mb-6">
                        <div className="mb-6">
                            <label className="text-gray-700 font-bold" htmlFor="name">Message</label>
                            <textarea rows={4} {...register('message')} className="w-full border border-gray-300 py-2 pl-3 rounded mt-2 outline-none" />
                            {errors.message && <div className="text-red-500">{errors.message?.message}</div>}
                        </div>
                    </div>

                    <div className="mb-6">
                        <label className="text-gray-700 font-bold" htmlFor="name">Photo <span className="text-sm font-normal text-gray-700"> (optional)</span> </label>
                        <input type="file" {...register('image')} className="w-full border border-gray-300 py-2 pl-3 rounded mt-2 outline-none" />
                        {errors.image && <div className="text-red-500">{errors.image?.message?.toString()}</div>}
                    </div>

                    <button disabled={isSubmitting} type="submit" className="disabled:opacity-50 w-full mt-6 text-indigo-50 font-bold bg-indigo-600 py-3 rounded-md hover:bg-indigo-500"><span> Submit</span></button>
                </div>
            </form>
        </div>
    )
}

这样修改后,当用户未选择文件时,image字段会被识别为undefined,不会触发任何文件验证规则;只有用户选择了文件时,才会依次检查文件是否存在、类型是否合法、大小是否符合要求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 21:57:02