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

如何用Zod+React Hook Form验证React Dropzone文件?验证失效求助

问题分析与修复

核心问题

你遇到的图片上传验证失效,主要有3个关键错误:

  • Zod Schema类型不匹配+缺少必填规则:定义的images是单个File类型,但实际上传的是File数组,且未添加非空验证
  • React Hook Form默认值未初始化:images字段默认值为空对象,不符合数组类型预期
  • FileUploader未同步表单状态:仅维护了预览URL,未跟踪实际文件数组,且缺少删除图片时的表单值更新逻辑

修正后的代码

1. Zod验证Schema

import { z } from "zod";
export const myFormSchema = z.object({
  // 改为File数组,且要求至少上传一张图片
  images: z.array(z.custom<File>()).nonempty("请至少上传一张图片"),
  title: z.string().nonempty("标题不能为空"), // 补充标题必填验证
});

2. FileUploader组件(新增文件状态跟踪与删除功能)

import { FileWithPath, useDropzone } from "react-dropzone";
import { useState, useCallback } from "react";
import Image from "next/image";
import { Upload, X } from "lucide-react";

type FileUploaderProps = {
  files: File[];
  onFilesChange: (files: File[]) => void;
};

const FileUploader = ({ files, onFilesChange }: FileUploaderProps) => {
  // 根据传入的文件生成预览URL
  const fileUrls = files.map(file => URL.createObjectURL(file));

  const onDrop = useCallback(
    (acceptedFiles: FileWithPath[]) => {
      onFilesChange(acceptedFiles);
    },
    [onFilesChange],
  );

  // 删除指定索引的文件,同步更新表单值
  const handleDeleteFile = (index: number) => {
    const updatedFiles = [...files];
    updatedFiles.splice(index, 1);
    onFilesChange(updatedFiles);
  };

  const { getRootProps, getInputProps } = useDropzone({
    onDrop,
    accept: {
      "image/*": [".png", ".jpeg", ".jpg", ".svg"],
    },
    maxFiles: 20, // 和标题提示的"最多20张"对应
  });

  return (
    <div
      {...getRootProps()}
      className="flex-center bg-dark-3 flex cursor-pointer flex-col rounded-xl p-4"
    >
      <input {...getInputProps()} className="cursor-pointer" />
      {files.length > 0 ? (
        <>
          <div className="grid grid-cols-3 gap-2">
            {fileUrls.map((image, index) => (
              <div key={index} className="relative group">
                <Image
                  alt="Product image"
                  className="aspect-square w-full rounded-md object-cover"
                  height="84"
                  src={image}
                  width="84"
                />
                {/* 删除按钮,hover时显示 */}
                <button
                  onClick={(e) => {
                    e.stopPropagation(); // 阻止触发dropzone的点击事件
                    handleDeleteFile(index);
                  }}
                  className="absolute top-1 right-1 bg-black/70 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
                >
                  <X className="h-3 w-3" />
                </button>
              </div>
            ))}
          </div>
          <p className="mt-10 text-center">Click or drag photo to replace</p>
        </>
      ) : (
        <div className="mt-2 grid grid-cols-3 gap-2">
          <div className="flex aspect-square w-full items-center justify-center rounded-md border border-dashed shadow">
            <Upload className="h-4 w-4 text-muted-foreground" />
            <span className="sr-only">Upload</span>
          </div>
        </div>
      )}
    </div>
  );
};

export default FileUploader;

3. 表单组件(修正默认值与组件传参)

'use client';
import { z } from 'zod';
import { useForm } from 'react-hook-form'; // 补充缺失的useForm导入
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@/components/ui/button';
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';

import FileUploader from '../shared/FileUploader';
import Link from 'next/link';
import { myFormSchema } from './path-to-your-schema'; // 替换为你的Schema实际路径

const InfoForm = () => {
  // 初始化表单默认值,匹配Schema类型
  const form = useForm<z.infer<typeof myFormSchema>>({
    resolver: zodResolver(myFormSchema),
    defaultValues: {
      images: [],
      title: "",
    },
  });

  async function onSubmit(values: z.infer<typeof myFormSchema>) {
    console.log('表单提交值:', values);
    // 这里可添加文件上传到后端的逻辑
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="title"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Title</FormLabel>
              <FormControl>
                <Input placeholder="输入标题" {...field} />
              </FormControl>
              <FormDescription>
                Mention the key features of your item (e.g. brand, model, age,
                type)
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="images"
          render={({ field }) => (
            <FormItem>
              <FormLabel>UPLOAD UP TO 20 PHOTOS</FormLabel>
              <FormControl>
                {/* 传递表单字段的value和onChange给上传组件 */}
                <FileUploader files={field.value} onFilesChange={field.onChange} />
              </FormControl>
              <FormMessage /> {/* 显示Zod的验证错误提示 */}
            </FormItem>
          )}
        />

        <div className="flex justify-between">
          <Link href="/">
            <Button variant="outline">Cancel</Button>
          </Link>
          <Button type="submit">Post</Button> {/* 明确指定submit类型 */}
        </div>
      </form>
    </Form>
  );
};

export default InfoForm;

额外说明

  • 补充了之前缺失的useForm导入,避免运行报错
  • 新增图片删除功能,删除后表单值会自动同步更新
  • Zod的nonempty规则会在表单提交时触发验证,错误提示自动显示在FormMessage中
  • 限制最大上传20张图片,和标题提示保持一致

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 09:47:05