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

使用shadcn-ui+react-hook-form+zod时表单提交模态框不关闭

问题描述

使用shadcn-ui的模态框,搭配react-hook-form和zod处理表单,目标是表单提交后自动关闭模态框,但当前无法实现。

代码文件展示

1. data-table.tsx(渲染模态框的组件)

import {
  ...
} from '@tanstack/react-table'
import { Container } from 'lucide-react'
import { useState } from 'react'

import { DataTablePagination } from '@/components/data-table-pagination'
import { Button } from '@/components/ui/button'
import { Dialog, DialogTrigger } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import {
  Table,
  ...
} from '@/components/ui/table'

import { NewSupplierModal } from '../../modals/NewSupplierModal'
import { useNewSupplierModalController } from '../../modals/NewSupplierModal/useNewSupplierModalController'

interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[]
  data: TData[]
}

export function SupplierDataTable<TData, TValue>({
  columns,
  data,
}: DataTableProps<TData, TValue>) {
  const [sorting, setSorting] = useState<ColumnSort[]>([])
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])

  const { isNewSupplierModalOpen, setIsNewSupplierModalOpen } =
    useNewSupplierModalController()

  const table = useReactTable({
    data,
    columns,
    ...
  })

  return (
    <div className="mx-auto container">
      <div className="flex justify-between items-center mb-[10px]">
        <Input
          placeholder="Buscar fornecedores"
          value={(table.getColumn('name')?.getFilterValue() as string) || ''}
          onChange={(e) => {
            table.getColumn('name')?.setFilterValue(e.target.value)
          }}
          className="max-w-sm"
        />

        <Dialog
          open={isNewSupplierModalOpen}
          onOpenChange={setIsNewSupplierModalOpen}
        >
          <DialogTrigger asChild>
            <Button variant="outline" size="xs">
              <Container className="mr-2 w-4 h-4" />
              Adicionar novo fornecedor
            </Button>
          </DialogTrigger>

          <NewSupplierModal />
        </Dialog>
      </div>

      <div className="mb-6 border rounded-md">
        <Table>
          ...
        </Table>
      </div>

      {/* Pagination */}
      <DataTablePagination table={table} />
    </div>
  )
}

2. useNewSupplierModalController.tsx(模态框逻辑控制)

import { zodResolver } from '@hookform/resolvers/zod'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import toast from 'react-hot-toast'
import * as z from 'zod'

import supplierService from '@/services/supplierService'
import { formSupplier } from '../../Table/SuppliersList/validators/form'

type Form = z.infer<typeof formSupplier>

export function useNewSupplierModalController() {
  const [isNewSupplierModalOpen, setIsNewSupplierModalOpen] = useState(false)

  const openNewSupplierModal = () => {
    setIsNewSupplierModalOpen(true)
  }

  const closeNewSupplierModal = () => {
    setIsNewSupplierModalOpen(false)
  }

  const queryClient = useQueryClient()

  const { mutateAsync, isPending } = useMutation({
    mutationFn: supplierService.create,
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ['get-suppliers'],
      })
    },
  })

  const form = useForm<Form>({
    resolver: zodResolver(formSupplier),
    defaultValues: {
      supplierName: '',
    },
  })

  async function onSubmit(data: Form) {
    try {
      await mutateAsync({
        name: data.supplierName,
      })

      toast.success('Fornecedor cadastrado com sucesso!')
      form.reset({
        supplierName: '',
      })
      closeNewSupplierModal()
    } catch (error) {
      toast.error('Erro ao enviar a requisição')
      form.reset({
        supplierName: '',
      })
    }
  }

  console.log({ isNewSupplierModalOpen })

  return {
    form,
    isPending,
    isNewSupplierModalOpen,
    onSubmit,
    openNewSupplierModal,
    closeNewSupplierModal,
    setIsNewSupplierModalOpen,
  }
}

3. NewSupplierModal.tsx(模态框界面组件)

import { Spinner } from '@/components/spinner'
import { Button } from '@/components/ui/button'
import {
  DialogContent,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog'
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'

import { useNewSupplierModalController } from '../../modals/NewSupplierModal/useNewSupplierModalController'

export function NewSupplierModal() {
  const { form, onSubmit, isPending } = useNewSupplierModalController()

  return (
    <DialogContent className="max-w-sm font-body">
      <DialogHeader>
        <DialogTitle>Novo Fornecedor</DialogTitle>
      </DialogHeader>

      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
          <FormField
            control={form.control}
            name="supplierName"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Nome do fornecedor</FormLabel>
                <FormControl>
                  <Input
                    placeholder="Informe o nome do fornecedor"
                    {...field}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />

          <Button type="submit" disabled={isPending} className="p-5 w-full">
            {isPending ? <Spinner /> : 'Cadastrar'}
          </Button>
        </form>
      </Form>
    </DialogContent>
  )
}

问题原因

你在两个组件中分别调用了useNewSupplierModalController:一次在SupplierDataTable,一次在NewSupplierModal。每个hook调用都会创建独立的状态实例,也就是说,SupplierDataTable中的isNewSupplierModalOpen和NewSupplierModal中的是完全不同的状态。当你在onSubmit里调用closeNewSupplierModal时,修改的是NewSupplierModal内的状态,而控制Dialog显示的是SupplierDataTable里的状态,所以模态框不会关闭。

解决方案

只在父组件SupplierDataTable中调用useNewSupplierModalController,然后将需要的属性传递给NewSupplierModal,避免重复调用hook导致状态不同步。

修改步骤:

  1. 更新SupplierDataTable组件
    从hook中取出所有需要的属性,并传递给NewSupplierModal:

    export function SupplierDataTable<TData, TValue>({
      columns,
      data,
    }: DataTableProps<TData, TValue>) {
      // ... 其他代码不变
      const { isNewSupplierModalOpen, setIsNewSupplierModalOpen, form, onSubmit, isPending } =
        useNewSupplierModalController()
    
      // ... 其他代码不变
    
      return (
        // ... 其他代码不变
        <Dialog
          open={isNewSupplierModalOpen}
          onOpenChange={setIsNewSupplierModalOpen}
        >
          <DialogTrigger asChild>
            {/* ... 按钮代码不变 */}
          </DialogTrigger>
          {/* 传递props给模态框 */}
          <NewSupplierModal form={form} onSubmit={onSubmit} isPending={isPending} />
        </Dialog>
        // ... 其他代码不变
      )
    }
    
  2. 更新NewSupplierModal组件
    移除内部的hook调用,改为接收props:

    import { Spinner } from '@/components/spinner'
    import { Button } from '@/components/ui/button'
    import {
      DialogContent,
      DialogHeader,
      DialogTitle,
    } from '@/components/ui/dialog'
    import {
      Form,
      FormControl,
      FormField,
      FormItem,
      FormLabel,
      FormMessage,
    } from '@/components/ui/form'
    import { Input } from '@/components/ui/input'
    import { useForm } from 'react-hook-form'
    import { formSupplier } from '../../Table/SuppliersList/validators/form'
    import * as z from 'zod'
    
    type Form = z.infer<typeof formSupplier>
    
    // 定义props类型
    interface NewSupplierModalProps {
      form: ReturnType<typeof useForm<Form>>
      onSubmit: (data: Form) => Promise<void>
      isPending: boolean
    }
    
    export function NewSupplierModal({ form, onSubmit, isPending }: NewSupplierModalProps) {
      return (
        <DialogContent className="max-w-sm font-body">
          <DialogHeader>
            <DialogTitle>Novo Fornecedor</DialogTitle>
          </DialogHeader>
    
          <Form {...form}>
            <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
              <FormField
                control={form.control}
                name="supplierName"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Nome do fornecedor</FormLabel>
                    <FormControl>
                      <Input
                        placeholder="Informe o nome do fornecedor"
                        {...field}
                      />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />
    
              <Button type="submit" disabled={isPending} className="p-5 w-full">
                {isPending ? <Spinner /> : 'Cadastrar'}
              </Button>
            </form>
          </Form>
        </DialogContent>
      )
    }
    

这样修改后,closeNewSupplierModal修改的就是控制Dialog显示的那个状态,表单提交后模态框就会自动关闭了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 18:44:57