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 — аварийный люк: используйте для фокуса/измерений/интеграций, не вместо состояния.