5 вещей, для которых вам не нужен React
Пять нативных API браузера заменяют частые React-компоненты: dialog, Popover, Custom Elements, container queries и View Transitions.
Браузерная платформа получила нативные, широкодоступные замены для нескольких UI-примитивов, которые раньше требовали компонентов React или сторонних библиотек: модальные диалоги, поповеры и тултипы, переиспользуемые виджеты без привязки к фреймворку, адаптивные макеты с учётом контейнера и анимированные переходы между представлениями. Эта статья — не аргумент против React: он по-прежнему остаётся правильным инструментом для сложного общего состояния, масштабных форм и экосистем вроде Next.js и Remix. Это чек-лист для аудита кодовой базы: пять категорий компонентов, которые у вас уже могут быть в React-проекте и с которыми браузер теперь справляется нативно — без единого килобайта дополнительного JavaScript-бандла.
Материал адресован практикующему React-разработчику, чья ментальная модель «что умеет браузер» перестала обновляться где-то в 2020 году. React 19 — текущий стабильный релиз, и ряд паттернов, которые решала его экосистема, теперь является частью самой платформы. В каждом разделе ниже описан привычный React-рефлекс, нативный API, который его заменяет, и конкретная оговорка по доступности, которую нужно учесть перед удалением кода.
Ключевые выводы
- HTML-элемент
<dialog>с методомshowModal()обеспечивает нативное удержание фокуса, закрытие по клавише Escape и псевдоэлемент::backdrop— устраняя большинство причин зависеть от React-библиотеки модальных окон. - Popover API отображает элементы в верхнем слое браузера (top layer), что полностью исключает целый класс багов с z-index и обрезкой при
overflow: hidden, которые преследуют самодельные React-тултипы и выпадающие меню. - Custom Elements с Shadow DOM позволяют поставлять один виджет, работающий в любом фреймворке или обычном HTML, без повторной реализации под каждый стек.
- CSS Container Queries (
@container) позволяют компоненту реагировать на ширину родительского элемента, заменяя хукиResizeObserverи состояние React, используемое исключительно для принятия решений о макете. - View Transitions API (
document.startViewTransition()) анимирует изменения состояния DOM нативно, покрывая многие сценарии, которые ранее решались с помощью Framer Motion илиreact-transition-group.
Модальные окна: используйте элемент <dialog> вместо библиотеки модалок
Для модальных диалогов нативный HTML-элемент <dialog>, вызываемый через showModal(), предоставляет удержание фокуса, инертность фонового контента, закрытие по Escape и стилизацию подложки — поведение, которое кастомная React-модалка должна реализовывать вручную и нередко реализует с ошибками. Элемент <dialog> входит в Baseline; уточните точную дату доступности на MDN перед публикацией внутренней документации.
Паттерн React. Команды, как правило, тянутся к react-modal, Radix Dialog или кастомному хуку useModal на основе портала. Паттерн с хуком обычно объединяет createPortal, useEffect, переключающий document.body.style.overflow, и написанную вручную ловушку фокуса. В записях пользовательских сессий такие реализации нередко демонстрируют, как пользователи переключаются табом из модального окна в фоновый контент — симптом неполной логики удержания фокуса.
Нативный API.
<dialog id="confirm" aria-labelledby="confirm-title">
<h2 id="confirm-title">Удалить проект?</h2>
<p>Это действие нельзя отменить.</p>
<form method="dialog">
<button value="cancel">Отмена</button>
<button value="confirm">Удалить</button>
</form>
</dialog>
<script>
document.getElementById('confirm').showModal();
</script>
showModal() помещает диалог в верхний слой (top layer), удерживает фокус внутри него, делает остальной документ инертным и отображает псевдоэлемент ::backdrop, который можно стилизовать через CSS. <form method="dialog"> закрывает диалог и возвращает значение value нажатой кнопки через dialog.returnValue — без каких-либо обработчиков событий.
Оговорки. Нюанс доступности заключается в том, что <dialog> не объявляет метку автоматически. Для корректной идентификации диалога программами чтения с экрана необходим атрибут aria-labelledby, указывающий на видимый заголовок (или атрибут aria-label). Если диалог немодальный — открытый через show() вместо showModal() — он не удерживает фокус, и в этом случае лучше рассмотреть Popover API. React или библиотека по-прежнему предпочтительнее, когда нужна декларативная логика открытия/закрытия, жёстко связанная с состоянием других компонентов, или анимация, выполняемая до размонтирования диалога.
Поповеры, тултипы и выпадающие меню: используйте Popover API
Popover API отображает элементы в верхнем слое браузера, что означает: поповер всегда появляется поверх остального контента вне зависимости от контекста наложения или overflow: hidden у родительского элемента. Это полностью устраняет целую категорию войн с z-index и багов с обрезкой, которые порождают самодельные реализации тултипов и выпадающих меню.
Паттерн React. Распространёнными зависимостями являются Floating UI, Radix Popover и оверлейные примитивы React-Aria. Они берут на себя позиционирование, закрытие по клику вне элемента и рендеринг через портал. Для простого тултипа это весьма значительный объём импортируемого кода.
Нативный API.
<button popovertarget="menu">Открыть меню</button>
<div id="menu" popover>
<a href="/account">Аккаунт</a>
<a href="/logout">Выйти</a>
</div>
Атрибут popover сам по себе — без единой строки JavaScript — даёт элемент, который переключается через кнопку с popovertarget, закрывается по клику снаружи и по Escape, и отображается в верхнем слое. Значение по умолчанию popover="auto" включает «лёгкое закрытие» (light-dismiss); popover="manual" отключает его для случаев, когда нужен явный контроль. Popover API имеет статус Baseline Newly Available; актуальный статус проверяйте в таблице совместимости MDN.
Оговорки. Нюанс доступности состоит в том, что в отличие от showModal() у <dialog>, Popover API не управляет фокусом автоматически. Если ваш поповер функционально является меню, вам всё равно нужно применить role="menu", управлять перемещением tabindex и переводить фокус в поповер при его открытии. Для позиционирования относительно триггера также потребуется CSS Anchor Positioning, статус Baseline которого более ограничен — проверьте на MDN перед использованием в кросс-браузерных сценариях. Для сложных меню с подменю, паттернами клавиатурной навигации и упреждающим вводом библиотека вроде Radix или React-Aria по-прежнему реально экономит усилия.
Discover how at OpenReplay.com.
Переиспользуемые виджеты: используйте Custom Elements и Shadow DOM
Custom Element, зарегистрированный через customElements.define(), работает в любом HTML-контексте — React, Vue, Angular, Svelte или обычном HTML-файле — без повторной реализации. В сочетании с Shadow DOM он обеспечивает инкапсуляцию стилей без CSS Modules, CSS-in-JS или шага сборки. Custom Elements и Shadow DOM имеют статус Baseline Widely Available; уточните год на MDN.
Web Components не вытеснили React в мейнстримной разработке приложений. Что они действительно заменили — так это необходимость поставлять один и тот же виджет пять раз, по одному на каждый фреймворк, когда вы поддерживаете дизайн-систему или распространяете сторонний встраиваемый компонент.
Паттерн React. Переиспользуемая кнопка, бейдж или график, обёрнутые в React-компонент, опубликованные в npm и повторно реализованные (или обёрнутые заново) для каждой команды, использующей другой фреймворк.
Нативный API.
class CopyButton extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' }).innerHTML = `
<style>button { padding: 6px 12px; }</style>
<button><slot>Copy</slot></button>
`;
this.shadowRoot.querySelector('button')
.addEventListener('click', () => {
navigator.clipboard.writeText(this.dataset.value ?? '');
});
}
}
customElements.define('copy-button', CopyButton);
Используется как <copy-button data-value="hello">Copy</copy-button> в любом HTML, включая JSX. React 19 поддерживает custom elements напрямую, включая передачу объектных пропсов и подписку на кастомные события.
Оговорки. Нюанс доступности заключается в том, что дерево доступности по умолчанию не пронизывает границы shadow DOM — ссылки aria-labelledby и aria-describedby в light DOM не могут указывать на ID внутри shadow root, и наоборот. Спецификация ARIA in HTML и находящееся в разработке предложение reference target решают эту проблему, однако на практике сегодня требуется либо явное указание ARIA-атрибутов на хост-элементе, либо использование attachInternals() с ElementInternals. React по-прежнему предпочтительнее, когда виджет должен тесно интегрироваться с состоянием приложения, разделять React Context или использовать Suspense.
Адаптивный макет на уровне компонента: используйте CSS Container Queries
CSS Container Queries (@container) позволяют компоненту адаптировать свой макет в зависимости от ширины собственного родителя, а не вьюпорта. Это устраняет паттерн с хуком useResizeObserver, где состояние React отслеживает размеры контейнера исключительно для управления именем класса. Container Queries имеют статус Baseline Widely Available — уточните год на MDN.
Паттерн React. Хук useResizeObserver (нередко из @react-hook/resize-observer или написанный вручную), подключённый к состоянию компонента, которое переключает проп layout="compact" или имя класса. Каждое изменение размера вызывает ре-рендер React, хотя единственным потребителем является CSS.
Нативный API.
.card-container {
container-type: inline-size;
}
.card {
display: grid;
grid-template-columns: 1fr;
}
@container (min-width: 400px) {
.card {
grid-template-columns: 120px 1fr;
}
}
Объявите container-type: inline-size на родительском элементе, затем пишите правила @container для дочернего. Браузер нативно обрабатывает наблюдение за изменением размеров. Никакого JavaScript, никаких ре-рендеров, никаких несоответствий при гидратации.
Селектор :has() дополняет этот подход для стилизации с учётом состояния. Правило вида form:has(input:invalid) button[type="submit"] { opacity: 0.5 } выражает то, что раньше требовало useState и паттерна контролируемого ввода. :has() имеет статус Baseline Widely Available — проверьте на MDN.
Оговорки. Нюанс доступности тонкий, но реальный: container queries могут кардинально менять макет без изменения порядка DOM, что хорошо для программ чтения с экрана, однако это означает, что вы всё равно должны проверять соответствие порядка чтения визуальному порядку на каждой контрольной точке. Container queries также вводят поведение containment, которое может влиять на расположение дочерних элементов, поэтому тестируйте компоненты, полагающиеся на позиционирование относительно вьюпорта или другие предположения о макете. Состояние React по-прежнему оправдано, когда решение о макете влечёт нечто большее, чем стилизация — например, когда нужно отрендерить другое дерево компонентов, а не просто перестилизовать существующее.
Анимированные переходы: используйте View Transitions API
View Transitions API оборачивает обновление DOM в анимацию перекрёстного растворения (cross-fade) по умолчанию, с полным управлением переходом через CSS посредством псевдоэлементов ::view-transition-*. Для переходов внутри одного документа он покрывает большинство анимаций переходов между маршрутами и состояниями, которые раньше требовали библиотек анимации.
Паттерн React. Framer Motion, react-transition-group или обёртки AnimatePresence вокруг компонентов маршрутов. Они работают, но требуют, чтобы анимация была выразима в рендер-модели React, что неудобно для переходов, охватывающих размонтирование одного дерева и монтирование другого.
Нативный API.
function navigate(url) {
if (!document.startViewTransition) {
updateDOM(url);
return;
}
document.startViewTransition(() => updateDOM(url));
}
document.startViewTransition() принимает колбэк, выполняющий обновление DOM. Браузер захватывает состояние «до», выполняет колбэк, захватывает состояние «после» и плавно переходит между ними. Чтобы анимировать конкретный элемент в рамках перехода — например, миниатюру, разворачивающуюся в детальный вид — присвойте совпадающим элементам одинаковое значение view-transition-name в CSS. Переходы внутри одного документа (same-document View Transitions) имеют статус Baseline Newly Available; переходы между документами (cross-document View Transitions) для MPA-навигаций поддерживаются шире — проверьте таблицу совместимости MDN и блог WebKit для актуального статуса Safari перед использованием кросс-документного режима.
Оговорки. Нюанс доступности связан с движением: соблюдайте медиазапрос prefers-reduced-motion, оборачивая переход в условие или полностью пропуская вызов для пользователей, отказавшихся от анимации. Стандартный cross-fade краткосрочен, но всё же является анимацией. React-библиотеки по-прежнему предпочтительнее, когда нужна физика пружин, переходы, управляемые жестами, или анимации, которые могут прерываться и обращаться в процессе воспроизведения — view transitions атомарны и не предназначены для этого.
Где React по-прежнему выигрывает
Пять замен, описанных выше, нацелены на конкретные категории компонентов. Для всего перечисленного ниже React по-прежнему является правильным инструментом, и замена его возможностями платформы обойдётся дороже, чем сэкономит.
- Сложное общее состояние между удалёнными компонентами. Когда несколько несвязанных частей UI подписаны на одно и то же изменяющееся состояние с производными селекторами, библиотеки вроде Zustand, Jotai или Redux Toolkit выполняют работу, которую платформа не делает. Кастомные события в Web Components могут передавать данные, но не моделируют производное состояние.
- Масштабные формы с кросс-полевой валидацией и динамическим рендерингом. Нативный
<form>, Constraint Validation API иFormDataчисто справляются с отправкой одиночной формы. Многошаговые мастера, условные поля, зависящие от значений в других частях формы, серверная валидация в сочетании с клиентской и массивы полей по-прежнему выигрывают от использования React Hook Form или TanStack Form. - Серверный рендеринг и получение данных. React Server Components, хук
use()для асинхронных данных и модель потокового SSR в Next.js и Remix решают проблемы координации гидратации, разделения кода и получения данных, которые платформа не решает напрямую. - Зрелость экосистемы для роутинга и слоёв данных. TanStack Router, TanStack Query и устоявшаяся экосистема React Router предоставляют инвалидацию кэша, оптимистичные обновления и паттерны загрузчиков маршрутов, воспроизведение которых поверх нативных API потребует значительных усилий.
- Командные соглашения и существующие инвестиции. Кодовая база, воронка найма, дизайн-система и CI, выстроенные вокруг React, сами по себе являются активом. Подход к аудиту здесь — удалять конкретные компоненты там, где платформа теперь справляется сама, а не мигрировать стеки целиком.
Практическое действие: откройте самую большую директорию React-компонентов, найдите Modal, Popover, Tooltip, Dropdown и любые импорты useResizeObserver. Каждый из них — кандидат на нативную замену, описанную выше. Проверьте статус Baseline соответствующего API на MDN для вашего диапазона поддерживаемых браузеров, выкатите замену под флагом функциональности и измерьте изменение размера бандла. Браузер догнал — оставшаяся работа заключается в аудите зависимостей, которые вам больше не нужны.
Часто задаваемые вопросы
Можно ли использовать нативный элемент dialog с моделью состояния React?
Да. Прикрепите ref к элементу dialog и вызывайте ref.current.showModal() или ref.current.close() из эффектов, управляемых состоянием React. Диалог остаётся в дереве React и принимает JSX-дочерние элементы в обычном режиме, однако вы обходите проп open, управляемый useState, на уровне рендеримого вывода. Основная сложность в том, что React не перезапускает эффекты для внутреннего события cancel диалога, поэтому подключите нативный обработчик close через useEffect для обратной синхронизации состояния.
Как Custom Elements передают сложные данные в React и обратно?
React 19 передаёт нестроковые пропсы напрямую в свойства custom element, а не сериализует их в атрибуты, поэтому объекты и массивы работают без JSON-кодирования. Custom elements передают данные обратно через CustomEvent, на которые React 19 подписывается с помощью стандартных пропсов-обработчиков с префиксом on (например, onMyEvent). В React 18 и более ранних версиях обработчики событий необходимо подключать императивно через ref, поскольку синтетические события не обрабатывают кастомные имена событий.
Влияют ли container queries и селектор :has() на производительность рендеринга?
Оба имеют измеримую стоимость, но в целом дешевле JavaScript-альтернатив, которые они заменяют. Container queries требуют от браузера поддержки контекста containment и повторного вычисления совпадающих правил при изменении размеров, что всё равно быстрее, чем колбэк ResizeObserver, вызывающий ре-рендер React. Селектор :has() может быть дорогостоящим при использовании с широкими селекторами субъекта в больших DOM-деревьях; ограничивайте его область конкретными родительскими элементами, а не применяйте к body или корневым элементам.
Работает ли View Transitions API с клиентскими роутерами вроде React Router?
Да, для переходов внутри одного документа. Оберните колбэк навигации вашего роутера в document.startViewTransition(), чтобы обновление DOM, которое React выполняет при смене маршрута, происходило внутри перехода. React Router v6 и TanStack Router поддерживают этот паттерн через перехват навигации. Кросс-документные view transitions, анимирующие полную загрузку страницы, требуют дополнительного включения через CSS-правило @view-transition и имеют более узкую поддержку браузеров — проверьте на MDN перед использованием.