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.
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>
// ...