React

Порталы и императивные рефы

Модальное окно живёт глубоко внутри карточки товара, у которой стоит overflow: hidden и z-index из соседнего блока. Окно открывается - и оказывается обрезанным, наполовину спрятанным под другими элементами. Знакомая боль: визуально элемент должен быть поверх всего, а структурно он застрял в чужом контейнере. Порталы решают именно это - позволяют отрендерить узел в любое место DOM, например прямо в body, сохранив его в дереве React там, где он логически принадлежит. А императивные рефы дают второй редкий, но нужный инструмент: дать родителю вызвать метод ребёнка напрямую.

  • Модальные окна и диалоги: рендерятся в body порталом, чтобы их не обрезал overflow и не давил z-index родителей
  • Тултипы и поповеры: позиционируются относительно элемента, но живут в корне DOM, чтобы не клипаться контейнером
  • Тосты и уведомления: единый контейнер в конце body, куда порталы складывают сообщения из любой части дерева
  • Дропдауны и меню: всплывающий слой выносится порталом поверх таблиц и форм со скроллом
  • Императивные рефы: фокус и валидация в формах, управление видеоплеером или canvas, методы вроде open/close у диалога

Предварительные знания

  • useRef: что такое ref и как он указывает на DOM-узел или хранит изменяемое значение
  • Понимание, что дерево React и реальное дерево DOM обычно совпадают по структуре
  • Всплытие событий в DOM: событие поднимается от цели вверх по предкам

Почему порталы и императивные рефы вообще появились

React намеренно декларативен и однонаправлен: данные текут вниз через props, а DOM строится из дерева компонентов. Но реальность подбрасывает случаи, которые в эту модель не укладываются. Модалке нужно вырваться из overflow родителя - так в React 16 (2017) появился ReactDOM.createPortal, который рендерит детей в другой DOM-узел, не меняя их места в дереве React. Другой случай: иногда родителю надо именно императивно дёрнуть ребёнка (сфокусировать поле, проиграть видео). Для этого ещё в эпоху рефов ввели useImperativeHandle - способ компоненту самому решить, какой набор методов он выставит наружу через ref. Оба инструмента - сознательные исключения из декларативной модели, и их рекомендуют применять точечно.

Портал: рендер вне DOM-родителя

Обычно дерево компонентов React превращается в дерево DOM один к одному: где компонент стоит в JSX, там его DOM-узел и окажется. Портал ломает это соответствие. createPortal принимает два аргумента - что рендерить и в какой существующий DOM-узел это поместить - и React вставляет результат туда, физически вынося его из родительского DOM-контейнера.

Зачем это нужно: CSS-свойства родителя могут испортить наложение элемента. overflow: hidden обрежет вылезающий тултип, локальный stacking context из-за transform или z-index спрячет модалку под соседним блоком. Если же узел вынесен прямиком в body, над ним нет ограничивающих контейнеров, и он спокойно перекрывает весь экран.

  • Без портала — Модалка - DOM-потомок карточки. Её обрезает overflow карточки, а z-index соседей легко перекрывает. Постоянная борьба с CSS.
  • С порталом — Модалка физически в body, над всеми контейнерами. CSS-ограничения родителя на неё не действуют, наложение предсказуемо.

Целевой узел можно создавать заранее в HTML (например, div с id portal-root рядом с корнем приложения) и рендерить порталы туда, а не в сам body. Это даёт чистую точку монтирования для всех оверлеев и упрощает стилизацию.

Какую проблему в первую очередь решает рендер модалки через портал в body?

События всплывают по дереву React, а не DOM

Самая неочевидная особенность порталов: хотя DOM-узел физически перенесён в другое место, в дереве React портал остаётся потомком там, где он объявлен. А события React всплывают по дереву React, не по дереву DOM. Поэтому клик внутри модалки, отрендеренной в body, всплывёт к тем родительским компонентам, внутри которых модалка описана в JSX - даже если в DOM эти родители находятся совсем в другой ветке.

Это не баг, а удобная гарантия. Контекст (Context) тоже идёт по дереву React, поэтому модалка в портале видит те же провайдеры темы, локали и стора, что и её логический родитель. Разработчик пишет код так, будто модалка находится на своём месте, и не думает о том, что в DOM она вынесена в body.

Практическое следствие: если над порталом в дереве React стоит обработчик, ловящий клики для закрытия чего-либо, клики из портала тоже до него дойдут. Иногда это нужно, иногда нет - и тогда всплытие останавливают через stopPropagation. Главное - помнить, что ориентиром служит дерево React, а не визуальное положение.

Расхождение двух деревьев сбивает с толку при отладке. В инспекторе DOM модалка лежит в body, а в React DevTools - внутри Card. Если искать причину всплывшего события по дереву DOM, легко зайти в тупик. Сверяться нужно с деревом React.

Кнопка внутри модалки отрендерена порталом в body. По какому дереву всплывёт событие клика по ней?

useImperativeHandle: выставить методы наружу

Обычно ref на компоненте указывает на его корневой DOM-узел. Но иногда родителю нужно не сам узел, а возможность вызвать у ребёнка действие: сфокусировать поле, проиграть видео, открыть диалог. useImperativeHandle позволяет компоненту самому решить, что окажется в ref родителя - вместо DOM-узла туда кладётся объект с выбранными методами.

Смысл в инкапсуляции. Родитель не получает доступ ко всему DOM-узлу ребёнка и не может делать с ним что угодно - он видит лишь узкий, осознанно выставленный интерфейс из focus и clear. Это императивный мостик, но с чёткими границами: компонент сам контролирует, что разрешено вызывать снаружи.

Императивные рефы - крайнее средство, а не повседневный инструмент. Большинство задач решается декларативно: передать значение через props и реагировать на его изменение. К useImperativeHandle прибегают только когда действие по своей природе императивно (фокус, скролл, управление воспроизведением, методы open/close), и описать его через состояние неестественно.

Что useImperativeHandle меняет в том, как работает ref на компоненте?

Связь с другими темами

Этот урок про два исключения из обычного потока. Рядом:

  • useRef — Императивный реф настраивает значение, которое родитель получает через ref - фундамент под useImperativeHandle
  • Компонент Activity — Тоже управляет поддеревом вне обычного рендера, но через сохранение состояния скрытых частей
  • Модель рендеринга — Портал делает дерево React и дерево DOM разными - понимание модели объясняет, почему это безопасно

Итог

  • createPortal рендерит детей в произвольный DOM-узел (часто body), не меняя их позицию в дереве React
  • Главная польза порталов - вырваться из overflow, clip и z-index родителя для модалок, тултипов и тостов
  • Несмотря на другое место в DOM, события всплывают по дереву React, а не по дереву DOM - контекст и обработчики родителя работают
  • useImperativeHandle позволяет компоненту выставить родителю набор методов через ref вместо узла DOM по умолчанию
  • Оба инструмента - исключения из декларативной модели, применять их стоит точечно, когда декларативный путь не подходит

Связанные уроки

  • rc-13-useref — useImperativeHandle настраивает то, что родитель получает через ref, поэтому сначала нужна модель самих рефов
  • rc-27-activity — Activity тоже про управление поддеревом вне обычного потока рендера, но через сохранение состояния, а не место в DOM
  • rc-10-render-mental-model — Портал ломает совпадение дерева React и дерева DOM, и понимание модели рендеринга помогает осознать это разделение
Порталы и императивные рефы

0

1

Войти