Комментарии 15
Как-то сложно. Я делал как в делфях, класс (хотя теперь говорят антипаттерн) модального окна, метод создания/загрузки и метод закрытия/выгрузки. Ну и метод showmodal. Никаких зависимостей, ну только что див надо указать куда грузить. В наследнике задаешь тайтл, иконку, кнопки с модальными резалтами, и собсна контент. Может немодно но удобно
Привет, спасибо за комментарий!
Да, на первый взгляд кажется сложно — но в статье я намеренно показал случаи, когда простого show(content) не хватает. В реальных проектах модалки редко сводятся к заголовку и двум кнопкам — внутри таблицы, формы, многошаговые флоу, разные API в зависимости от того, откуда открыли. Передать всё это через один метод можно, но конфиг разрастается, и логика "что показать" начинает течь внутрь модалки. Как раз это я и пытался решить.
ИМХО модальные окна - для диалогов. Не в смысле что там две кнопки и вопрос. А в том смысле, что оно предназначено для изменения или передачи информации. Оно отбражает инфу, позволяет ее изменять, валидировать. И отдать результат обратно. А что с этим всем потом делать, это уже реализуется снаружи. Иначе сложность системы будет неконтролируемо расти.
Спасибо большое за статью, откликнулось)
У меня в проекте все те кейсы встречаются, которые вы описали. Видов (компонентов) модальных окон - 90 штук, а мест где они вызываются наверное где-то под 200. Проекте правда на vue3
Есть только один вопрос, возможно ламерский, но все же.
Пример из статьи:
const openModal = useCallback(
(id: number) =>
open({ name: EOverlayName.EditProduct, data: { productId: id } }),
[open],
);В этом случае, разве реактивность у productId не теряется? А что если пока была открыта модалка, стейт поменялся?
Приветствую, да все так. Но те данные которые мы передаем, в данном подходе через open() в большинстве случаев не поменяются пока мы работает с модальным окном.
Эти данные нужны единожды при открытии, чтобы понять с какой сущностью мы работаем - в данном случае productId, а он вряд-ли поменяется пока наше окно открыто.
Очевидно: модалку нельзя рендерить в каждой строке.
Можно же возвращать из компонента null пока модалка не открыта и не нужна?
Пожалуй хуки, которые очевидно будут внутри модалки до null – не бесплатные. Можно попробовать сделать в 2 компонента, сначала обёртку, которая будет возвращать или null или модалку, а внутри модалки уже нужные хуки, запросы и т.п., но до первых анимаций – уже какую-то задержку вводить в обёртке.
Про анимацию немного не понял, о чём речь
Анимация ведь уже асинхронная, значит где-то надо обработать начало анимации при закрытии модалки, и только после её окончания вернуть null. Иначе вход модалки будет с анимацией, а выход уже без. Ну и вот эта обработка, скорее всего будет дополнительным стейтом вокруг модалки в компоненте-обёртке, если бы мы так делали.
Понял, я что-то подумал, что «первых анимаций» — это про анимацию открытия.
Засовывать в обёртку какую-то обработку событий — это не дело конечно, но это и не значит, что она была бы полностью бесполезной. Например, можно возвращать null и не монтировать «тяжёлую» часть можно только пока компонент не был виден ни разу.
Модалка не открывалась ни разу: Обёртка возвращает
nullМодалка открыта: Обёртка возвращает return value модалки.
Модалка снова закрыта: Обёртка возвращает return value модалки, но он равен
null.
Чтобы хранить состояние «было открыто хотя бы раз», нужен один реф на каждый экземпляр обёртки. Не очень много.
После закрытия модалки её стейт останется в памяти — это да, но сколько модалок юзер должен протыкать вручную, чтобы это стало ощутимо заметно?
Спасибо за коммент. Если мы будем возвращать null - данный подход не дружит к сожалению с анимациями - а в частности исчезновения, поэтому будут проблемки, если у модального окна присутствует анимация.
Также, хоть мы и вернули бы null, но стейт у компонента остается - это не бесплатно, к сожалению. Поэтому данный подход не сильно спасет ситуацию.
Пожалуйста не надо модальных окон
Хватит мучать и бесить пользователей
Прошу
В большинстве проектов контекст 1, и в связи с этим я не понимаю зачем всё это усложнение в виде множества обёрток..
Проще и чище реализовать классы Singleton, их можно как нативно, так и через фабрику (GetService(SomeService))
Далее создаём класс контроллер, который в себе хранит массивы открытых модалок, а сами модалки отображаем через react порталы
И всё, у нас чистые методы: Open, Close и так далее..
Можно создать универсальный метод OpenModal, а затем создать множество специализированных методов: OpenAlert, OpenError и т.д.
И в коде с использованием await их вызывать
В идеале перейти на использование ViewModel, можно как собственную реализацию через useMemo + useState, так и взять готовую, например mobx
После чего у нас будут чистые view, которые отвечают строго за отображение, а вся логика обработки будет находится во viewModel.. код станет чище и понятнее, да и даст возможность переиспользовать как сам view отдельно от viewModel, так и наоборот
Цепочка 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, проблема композиции компонентов, проблема разделения ответственности. Каждая из них должна решаться индивидуально используя различные паттерны и подходы к композиции компонентов.

Модальные окна в React: архитектура управления для сложных интерфейсов