Next.js 14中如何避免Zod校验错误时Shadcn Sheet关闭
解决方案:阻止Shadcn Sheet在校验错误时自动关闭
核心问题分析
你的代码存在两个关键问题:
- 提交按钮直接绑定了关闭Sheet的逻辑,忽略了表单校验状态;
- 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> </> ) }
修改说明
- 提交按钮逻辑修复:移除了提交按钮上直接关闭Sheet的
onClick事件,只有当表单校验通过并提交成功时,才在onSubmit函数中关闭Sheet。校验不通过时,react-hook-form会自动显示错误,Sheet保持打开。 - Cancel按钮修复:修正了错误的状态设置逻辑,直接使用
SheetClose组件原生关闭行为,同时调整按钮样式区分主次操作。 - Sheet关闭拦截:将Sheet改为受控模式,通过
onOpenChange回调监听关闭请求,若表单存在校验错误则阻止关闭。 - 错误状态传递:通过回调将表单错误状态传递给父组件,用于判断是否允许关闭Sheet。
内容的提问来源于stack exchange,提问作者Me Unagi
相关产品推荐
相关产品推荐

