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

Next.js 14中如何避免Zod校验错误时Shadcn Sheet关闭

解决方案:阻止Shadcn Sheet在校验错误时自动关闭

核心问题分析

你的代码存在两个关键问题:

  1. 提交按钮直接绑定了关闭Sheet的逻辑,忽略了表单校验状态;
  2. Sheet组件未结合表单错误状态做关闭拦截,导致点击关闭按钮/外部区域时直接关闭。

以下是针对性的代码修改:


修改后的 CellAction.tsx

"use client";

import { CategoryForm } from "@/components/forms/category-form";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Sheet, SheetTrigger } from "@/components/ui/sheet"
import { Category } from "@/constants/data";
import { Edit, MoreHorizontal, Trash } from "lucide-react";
import { useState } from "react";
import * as z from "zod";

interface CellActionProps {
  data: Category;
}
export const CellAction: React.FC<CellActionProps> = ({ data }) => {
  const [open, setOpen] = useState(false);
  const [isOpen, setIsOpen] = useState(false);
  const [formErrors, setFormErrors] = useState<z.ZodError<any> | undefined>(undefined);

  return (
    <>
      <Sheet 
        open={isOpen} 
        onOpenChange={(newOpen) => {
          // 尝试关闭时检查表单错误,有错误则阻止关闭
          if (!newOpen && formErrors && Object.keys(formErrors).length > 0) {
            return;
          }
          setIsOpen(newOpen);
        }}
      >
        <DropdownMenu modal={false}>
          <DropdownMenuTrigger asChild>
            <Button variant="ghost" className="h-8 w-8 p-0">
              <span className="sr-only">Open menu</span>
              <MoreHorizontal className="h-4 w-4" />
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end">
            <DropdownMenuLabel>Actions</DropdownMenuLabel>
            <DropdownMenuItem>
              <SheetTrigger>
                <div className="flex"><Edit className="mr-2 h-4 w-4"/>Edit</div>
              </SheetTrigger>
            </DropdownMenuItem>
            <DropdownMenuItem onClick={() => setOpen(true)}>
              <Trash className="mr-2 h-4 w-4" /> Delete
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>

        <CategoryForm 
          initialData={data} 
          setIsOpen={setIsOpen} 
          isOpen={isOpen}
          onErrorsChange={setFormErrors}
        />
      </Sheet>
    </>
  );
};

修改后的 CategoryForm.tsx

"use client";
import * as z from "zod";
import { useState, useEffect } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useParams, useRouter } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useToast } from "../ui/use-toast";
import { SheetContent, SheetDescription, SheetHeader, SheetFooter, SheetTitle, SheetClose, } from "@/components/ui/sheet"
import React, { Dispatch, SetStateAction } from 'react'

const formSchema = z.object({
  name: z
    .string()
    .min(3, { message: "Category Name must be at least 3 characters" }),
  status: z.string().min(1, { message: "Please select a Status" }),
});

type CategoryFormValues = z.infer<typeof formSchema>;

interface CategoryFormProps {
  initialData: any | null;
  isOpen: boolean;
  setIsOpen: Dispatch<SetStateAction<boolean>>;
  onErrorsChange?: (errors: z.ZodError<CategoryFormValues> | undefined) => void;
}

export const CategoryForm: React.FC<CategoryFormProps> = ({
  initialData,
  setIsOpen,
  isOpen,
  onErrorsChange
}) => {
  const params = useParams();
  const router = useRouter();
  const { toast } = useToast();
  const [loading, setLoading] = useState(false);
  const title = initialData ? "Edit product" : "Create product";
  const description = initialData ? "Edit a product." : "Add a new product";
  const toastMessage = initialData ? "Product updated." : "Product created.";
  const action = initialData ? "Save changes" : "Create";
  
  const defaultValues = initialData
    ? initialData
    : {
        name: "",
        status: "",
      };
  
  const form = useForm<CategoryFormValues>({
    resolver: zodResolver(formSchema),
    defaultValues,
  });

  const { formState: { errors } } = form;

  // 监听表单错误变化,传递给父组件
  useEffect(() => {
    onErrorsChange?.(errors as z.ZodError<CategoryFormValues>);
  }, [errors, onErrorsChange]);

  const onSubmit = async (data: CategoryFormValues) => {
    try {
      setLoading(true);
      if (initialData) {
        // await axios.post(`/api/categories/${initialData._id}`, data);
      } else {
        // const res = await axios.post(`/api/products/create-product`, data);
      }

      router.refresh();
      /// 只有提交成功才关闭Sheet
      setIsOpen(false);

      toast({
        variant: "default",
        title: toastMessage,
        description: toastMessage
      });
    } catch (error: any) {
      /// 提交失败保持Sheet打开
      toast({
        variant: "destructive",
        title: "Uh oh!! Something went wrong.",
        description: "There was a problem with your request.",
      });
    } finally {
      setLoading(false);
    }
  };

  return (
    <>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>{title}</SheetTitle>
          <SheetDescription>
            {description}
          </SheetDescription>
        </SheetHeader>

        <Form {...form}>
          <form
            onSubmit={form.handleSubmit(onSubmit)}
            className="space-y-8 w-full"
          >            
            <div className="grid gap-4 py-4">
              <FormField
                control={form.control}
                name="name"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Name</FormLabel>
                    <FormControl>
                      <Input
                        disabled={loading}
                        placeholder="Category name"
                        {...field}
                      />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />    
              <FormField
                control={form.control}
                name="status"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Status</FormLabel>
                    <Select
                      disabled={loading}
                      onValueChange={field.onChange}
                      value={field.value}
                      defaultValue={field.value}
                    >
                      <FormControl>
                        <SelectTrigger>
                          <SelectValue
                            defaultValue={field.value}
                            placeholder="Select a status"
                          />
                        </SelectTrigger>
                      </FormControl>
                      <SelectContent>
                        <SelectItem key="Active" value="Active">Active</SelectItem>
                        <SelectItem key="Inactive" value="Inactive">Inactive</SelectItem>
                      </SelectContent>
                    </Select>
                    <FormMessage />
                  </FormItem>
                )}
              />
            </div>
            <SheetFooter>
              <Button  
                disabled={loading} 
                className="ml-auto" 
                type="submit"
              >
                {action}
              </Button>
              <SheetClose asChild>
                <Button  
                  disabled={loading} 
                  variant="secondary"
                  className="ml-2"
                >
                  Cancel
                </Button>
              </SheetClose>
            </SheetFooter>
          </form>
        </Form> 
      </SheetContent>                  
    </>
  )
}

修改说明

  1. 提交按钮逻辑修复:移除了提交按钮上直接关闭Sheet的onClick事件,只有当表单校验通过并提交成功时,才在onSubmit函数中关闭Sheet。校验不通过时,react-hook-form会自动显示错误,Sheet保持打开。
  2. Cancel按钮修复:修正了错误的状态设置逻辑,直接使用SheetClose组件原生关闭行为,同时调整按钮样式区分主次操作。
  3. Sheet关闭拦截:将Sheet改为受控模式,通过onOpenChange回调监听关闭请求,若表单存在校验错误则阻止关闭。
  4. 错误状态传递:通过回调将表单错误状态传递给父组件,用于判断是否允许关闭Sheet。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 19:03:15