Portals и refs (forwardRef, useImperativeHandle)

Краткий урок про два «выхода за рамки» обычного дерева: portals (рендер в другой узел DOM) и refs для императивного доступа (forwardRef, useImperativeHandle).

Portal рендерит детей в DOM-узел вне родителя по дереву. Ref даёт прямую (императивную) ссылку на DOM-узел или на методы дочернего компонента.

Portals: зачем

Модальные окна, тултипы, выпадающие меню страдают от родительских overflow: hidden, z-index и transform — они «обрезаются» контейнером. createPortal позволяет отрисовать UI в другой узел DOM (обычно прямо в body), сохранив при этом положение в React-дереве: контекст, состояние и события всплывают как обычно, будто портала нет.

import { createPortal } from "react-dom";

function Modal({ children, onClose }) {
  return createPortal(
    <div className="backdrop" onClick={onClose}>
      <div className="dialog" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body // второй аргумент — целевой DOM-узел
  );
}

Ключевой нюанс: события из портала всплывают по React-дереву, а не по DOM. Поэтому onClick на родителе-компоненте поймает клик из модалки, хотя в DOM она лежит в body.

Refs и forwardRef

Иногда нужно императивно дотянуться до DOM-узла: сфокусировать input, измерить размер, запустить воспроизведение видео. useRef даёт ref на собственный элемент. Но если вы хотите передать ref в свой компонент и довести его до внутреннего DOM-узла, обычный проп не подойдёт — ref особенный. Здесь нужен forwardRef.

const TextInput = React.forwardRef(function TextInput(props, ref) {
  return <input ref={ref} {...props} />;
});

function Form() {
  const inputRef = React.useRef(null);
  return (
    <>
      <TextInput ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Фокус</button>
    </>
  );
}

useImperativeHandle: отдать только нужные методы

Иногда не хочется отдавать наружу весь DOM-узел — лучше предоставить узкий «императивный API». useImperativeHandle в паре с forwardRef определяет, что именно увидит родитель через ref.

const Player = React.forwardRef(function Player(props, ref) {
  const videoRef = React.useRef(null);
  React.useImperativeHandle(ref, () => ({
    play: () => videoRef.current.play(),
    pause: () => videoRef.current.pause(),
  }));
  return <video ref={videoRef} src={props.src} />;
});
// родитель: playerRef.current.play() — но не доступ ко всему <video>

Когда это нужно — и когда нет

  • Refs — это аварийный люк в императивный мир. Большинство задач решается состоянием и пропсами. Тянитесь к ref только для фокуса, измерений, скролла, интеграции с не-React библиотеками.
  • Не управляйте через ref тем, что можно выразить декларативно (видимость, классы, текст) — это против духа React.
  • useImperativeHandle применяйте редко: только когда родителю действительно нужен компактный набор команд к ребёнку.

Итог

  • createPortal рендерит UI в другой DOM-узел, сохраняя место в React-дереве и всплытие событий.
  • forwardRef пробрасывает ref сквозь компонент к внутреннему элементу.
  • useImperativeHandle отдаёт наружу узкий императивный API вместо всего узла.
  • Refs — аварийный люк: используйте для фокуса/измерений/интеграций, не вместо состояния.
Проверьте себя
1. Что особенного в событиях из portal?
AОни не работают вовсе
BОни всплывают по React-дереву, а не по DOM
CОни всегда дублируются
DИх нужно ловить вручную
2. Зачем нужен forwardRef?
AЧтобы передать обычный проп
BЧтобы пробросить ref сквозь компонент к внутреннему DOM-элементу
CЧтобы создать контекст
DЧтобы мемоизировать компонент
3. Что делает useImperativeHandle?
AОткрывает весь DOM-узел наружу
BОпределяет узкий императивный API (например, play/pause), который родитель увидит через ref
CЗаменяет useState
DСоздаёт portal
Поддержать проект