Search code examples
reactjstypescriptreact-hook-formzodshadcnui

Modal doesn't close on submission


I'm using shadcn-ui to create my modal along with react-hook-form and zod.

My main goal is for the modal to close when the form is submitted.

I render my modal inside my 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>
  )
}

This is where all the logic of my modal is located:

import { zodResolver } from '@hookform/resolvers/zod'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { FormEvent, FormEventHandler, useCallback, 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,
  }
}

And this is its interface:

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>
  )
}

I don't know if it's some state or function that I've passed to my open and onOpenChange props, but so far I've had no success.

Thank you in advance for your help.


Solution

  • The confusion is coming from that you use the useNewSupplierModalController() twice. Once in SupplierDataTable, which is the one that is actually driving the visibility of the modal; and again in NewSupplierModal.

    Every time you use a hook, it has its own fresh state. They are, in a way, "instances". If you use it in more than one place that means you've created new state entirely. Custom hooks are a way to abstract common logic into a reusable unit. They are not a mechanism to share state.

    Since the submit handler in NewSupplierModal talks to the hook used inside of that same component, it has no effect on the hook used in SupplierDataTable (which drives the dialogue visibility). It will instead switch the isNewSupplierModalOpen inside the instance of the hook where the submit handler was grabbed. And that isn't even connected to the actual Dialog props that control its visibility.

    Remove the useNewSupplierModalController call inside NewSupplierModal and keep the one in SupplierDataTable. Then pass through the necessary things from the one in SupplierDataTable to NewSupplierModal via props instead.

    Inside data-table.tsx:

    export function SupplierDataTable<TData, TValue>({
      columns,
      data,
    }: DataTableProps<TData, TValue>) {
      const [sorting, setSorting] = useState<ColumnSort[]>([])
      const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
    
      const { isNewSupplierModalOpen, setIsNewSupplierModalOpen, form, onSubmit, isPending } =
        useNewSupplierModalController()
    
    // ...
    
        <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 form={form} onSubmit={onSubmit} isPending={isPending}/>
            </Dialog>
    
    // ...
    
    

    Inside NewSupplierModal file:

    export function NewSupplierModal({form, onSubmit, isPending}) {
      return (
        <DialogContent className="max-w-sm font-body">
          <DialogHeader>
            <DialogTitle>Novo Fornecedor</DialogTitle>
    
    // ...