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, });
关键修改说明
- 用
z.union区分两种状态:明确允许字段为undefined(对应未选择文件),同时对选中文件的情况(FileList实例)执行完整验证。 - 移除
.optional():因为z.union已经包含了undefined的可选情况,不需要额外加.optional()。 - 直接使用
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
相关产品推荐
相关产品推荐

