Обновить
0
Александр@AlexAbashin

Software Engineer

Отправить сообщение

Цепочка ProductsPage -> ProductsTable -> TableBody -> ProductRow -> ActionsMenu — признак легаси. В современном React таблицу можно сконфигурировать через, например TanStack Table, а не через вложенные компоненты.

Накидал небольшой пример. Стейт модалок — локальный, рядом с триггером. Не нужно тащить информацию о модалках в Redux. В данном случае передача itemId в дочерний компонент — это не props drilling, а вполне корректная передача данных туда где они нужны. В 90% случаев в современном React локальный стейт лучше глобального, большую часть проблем закрывает кэш (например TanStack Query), а не стейт.

const Page = () => {
  const { data, isLoading } = useData()
  const [selectedItems, setSelectedItems] = useState<string[]>([])

  const columns = useMemo<ColumnDef<Item>[]>(
    () => [
      { header: 'Название', accessorKey: 'name' },
      {
        id: 'actions',
        cell: ({ row }) => <ActionsMenu item={row.original} />,
      },
    ],
    [],
  )

  if (isLoading) return <Spinner />

  return (
    <PageLayout>
      <PageLayoutHeader>
        <DeleteAction itemIds={selectedItems} />
      </PageLayoutHeader>
      <PageLayoutContent>
        <Table
          columns={columns}
          data={data}
          onRowSelectionChange={setSelectedItems}
        />
      </PageLayoutContent>
    </PageLayout>
  )
}

const DeleteAction = ({ itemIds }: { itemIds: string[] }) => {
  const { deleteItems } = useDeleteItems()
  const { isOpen, open, close } = useDialog()

  return (
    <>
      <Button onClick={open}>Удалить</Button>
      <ConfirmationModal
        isOpen={isOpen}
        onClose={close}
        onConfirm={() => deleteItems(itemIds)}
      />
    </>
  )
}

const ActionsMenu = ({ item }: { item: Item }) => {
  const { isOpen, open, close } = useDialog()

  return (
    <>
      <Button onClick={open}>Редактировать</Button>
      <EditModal itemId={item.id} isOpen={isOpen} onClose={close} />
    </>
  )
}

// DialogContent (например возьмем Radix/shadcn) не размонтируется при close —
// children монтируются лениво, анимации работают из коробки.
// Содержимое выносим в отдельный компонент, чтобы хуки и запросы
// выполнялись только при открытии.
const EditModal = ({ itemId, isOpen, onClose }: {
  itemId: string; isOpen: boolean; onClose: () => void
}) => (
  <Dialog open={isOpen} onOpenChange={onClose}>
    <DialogContent>
      <EditModalContent itemId={itemId} />
    </DialogContent>
  </Dialog>
)

const EditModalContent = ({ itemId }: { itemId: string }) => {
  const { data, isLoading } = useData(itemId)
  const mutate = useMutateData()
  const { isOpen, open, close } = useDialog()

  if (isLoading) return <Spinner />

  return (
    <>
      {/* контент */}
      <Button onClick={open}>Подтвердить</Button>
      <ConfirmationModal
        isOpen={isOpen}
        onClose={close}
        onConfirm={() => mutate(itemId)}
      />
    </>
  )
}

На тех объёмах, где таблица начнёт тормозить (и потребует виртуализации), пара лишних DOM-нод от Dialog — это погрешность. Реальная проблема — рендер самих строк, а не обёрток модалок.

П.С. В статье решается очень много проблем разом, проблема плохого UX, проблема композиции компонентов, проблема разделения ответственности. Каждая из них должна решаться индивидуально используя различные паттерны и подходы к композиции компонентов.

Информация

В рейтинге
Не участвует
Откуда
Тула, Тульская обл., Россия
Дата рождения
Зарегистрирован
Активность