Я использую shadcn-ui для создания модального окна вместе с React-Hook-Form и Zod.
Моя главная цель — закрыть модальное окно при отправке формы.
Я визуализирую модальное окно внутри 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>
)
}
Здесь находится вся логика моего модального окна:
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.info({ isNewSupplierModalOpen })
return {
form,
isPending,
isNewSupplierModalOpen,
onSubmit,
openNewSupplierModal,
closeNewSupplierModal,
setIsNewSupplierModalOpen,
}
}
А это его интерфейс:
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>
)
}
Я не знаю, передал ли я в свои реквизиты open и onOpenChange какое-то состояние или функцию, но до сих пор мне это не удалось.
Заранее спасибо за вашу помощь.
Путаница возникает из-за того, что вы используете useNewSupplierModalController()
дважды. Однажды в SupplierDataTable
, который на самом деле обеспечивает видимость модального окна; и снова в NewSupplierModal
.
Каждый раз, когда вы используете хук, он приобретает новое состояние. В каком-то смысле они являются «экземплярами». Если вы используете его более чем в одном месте, это означает, что вы полностью создали новое состояние. Пользовательские перехватчики — это способ абстрагировать общую логику в модуль многократного использования. Они не являются механизмом разделения состояния.
Поскольку обработчик отправки в NewSupplierModal
взаимодействует с хуком, используемым внутри того же компонента, он не влияет на хук, используемый в SupplierDataTable
(который управляет видимостью диалога). Вместо этого он переключит isNewSupplierModalOpen
внутри экземпляра хука, где был захвачен обработчик отправки. И это даже не связано с реальным Dialog
реквизитом, контролирующим его видимость.
Удалите вызов useNewSupplierModalController
внутри NewSupplierModal
и сохраните его SupplierDataTable
. Затем вместо этого передайте необходимые вещи из SupplierDataTable
в NewSupplierModal
через реквизит.
Внутри 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>
// ...
Внутри NewSupplierModal
файла:
export function NewSupplierModal({form, onSubmit, isPending}) {
return (
<DialogContent className = "max-w-sm font-body">
<DialogHeader>
<DialogTitle>Novo Fornecedor</DialogTitle>
// ...
Я выяснил, как правильно типизировать свойство form: это UseFormReturn, который является типом, возвращаемым функцией useForm библиотеки реакции-хука-формы, а затем Form, который является типом, выведенным из моей схемы проверки. Minhas типагенс ficaram assim: интерфейс NewSupplierModalProps { onSubmit: (data: Form) => void form: UseFormReturn<Form> isPending: boolean }
Поскольку я использую эту модальную структуру в других местах, могу ли я создать крючок, чтобы упростить ее? Например, мне пришлось бы создавать подобные интерфейсы в каждом модальном окне, которое я создаю, получая onSubmit, form и isPending. Стоит ли это создавать или не обязательно?
Вы можете просто создать для этого псевдоним типа и повторно использовать этот псевдоним типа в нескольких компонентах (используйте &
для пересечения с дополнительными реквизитами). Или ts интерфейсы и расширяющий синтаксис
Чувак, я пытался использовать ту же логику в другой части моего кода, где я визуализирую два модальных окна с разными триггерами. Возникла проблема, когда я передал свойство form в мой модальный компонент. Это ошибка ввода, поскольку изначально я получаю свойство типа Date, которое хочу отправить в API в виде строки. Знаешь, как я могу это решить?
Я использую этот метод zod, z.input и z.output, чтобы вставить схему ввода и вывода, но все равно получаю ту же ошибку.
Как будет выглядеть мой интерфейс NewSupplierModal? Примерно так: интерфейс NewSupplierModalProps {onSubmit: (данные: Форма) => void; form: //... isPending: boolean; } Я не знаю, как напечатать форму.