Synthetic Events в React: как работает система событий
Разбираем Synthetic Events в React: делегирование, bubbling/capturing, приоритеты обновлений, интеграция с native events и production-ошибки.
- Введение
- Что такое Synthetic Events и зачем они React
- Короткий ответ
- Что React выигрывает от такого слоя
- Что важно помнить
- Архитектура: как React обрабатывает событие
- Делегирование событий: что изменилось после React 17
- Capture и Bubble в React на практике
- Работа с Synthetic Event объектом
- Таблица сравнения: Synthetic Events и альтернативы
- Production pitfalls: где чаще всего ломается система событий
- Ошибка 1. Смешивание React-обработчиков и native listeners без явной границы
- Ошибка 2. Отсутствие cleanup для нативных подписок
- Ошибка 3. Тяжелая логика прямо в onChange/onMouseMove
- Ошибка 4. Неучтенные особенности portal и modal-слоев
- Разбор производительности
- Когда Synthetic Events не закрывают задачу
- Практики, которые работают в командах
- Частые ошибки
- Как отвечать на интервью про Synthetic Events
- Практический кейс из production: outside click для dropdown
- Чек-лист внедрения event-логики в существующий код
- FAQ
- Synthetic Event полностью заменяет native event?
- Нужно ли всегда писать onClickCapture?
- Почему stopPropagation не всегда останавливает «все»?
- Есть ли смысл использовать native listeners внутри React-приложения?
- Что важнее для производительности: Synthetic Events или оптимизация рендеров?
- Итоги
Введение
Тема Synthetic Events в React регулярно недооценивается: многие знают только факт, что «React оборачивает DOM-события». На практике этого недостаточно, потому что ошибки обычно возникают в интеграции React-обработчиков с нативными listeners, порталами, сложными формами и сторонними виджетами.
Если вы работаете с интерактивными интерфейсами, понимание системы событий напрямую влияет на стабильность UX и на предсказуемость рендеров. Для общего контекста полезно параллельно изучить разбор жизненного цикла компонента, материал про batching обновлений и статью о том, когда React реально ререндерит компонент.
В этом разборе пройдем весь путь: что такое Synthetic Events, как устроено делегирование, где границы с native DOM API и какие production-паттерны реально работают в командах.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью.
Что такое Synthetic Events и зачем они React
Короткий ответ
Synthetic Event — это объект события, который React передает в обработчики (onClick, onChange, onKeyDown и т.д.) вместо прямой работы с браузерным событием. Он нормализует API между браузерами и встроен в систему обновлений React.
Что React выигрывает от такого слоя
- единый интерфейс событий между браузерами;
- централизованное делегирование (меньше отдельных listeners в DOM);
- связка события с приоритетом обновлений и планировщиком рендера;
- предсказуемое поведение в дереве React-компонентов.
Что важно помнить
Synthetic Event не «отменяет» нативный DOM. Внутри есть ссылка на исходное событие (nativeEvent), а в сложных сценариях нужно понимать обе модели одновременно: React-цепочку и native propagation.
Архитектура: как React обрабатывает событие
Упрощенная схема такая:
- Браузер генерирует native event.
- React ловит его на уровне делегированного listener.
- Создается Synthetic Event с нормализованными полями.
- React проходит фазу capture и bubble по дереву React.
- Из обработчика ставятся обновления state в очередь React.
- Планировщик определяет, когда выполнить рендер и commit.
Ключевые компоненты архитектуры:
- слой делегирования событий;
- механизм извлечения и нормализации события;
- маршрутизация обработчиков capture/bubble;
- интеграция с приоритетами обновлений.
Точка частого отказа: разработчик предполагает, что stopPropagation в одном месте остановит вообще все обработчики в приложении. Это неверно, если часть логики живет в нативных listeners вне React-root.
Делегирование событий: что изменилось после React 17
До React 17 React вешал многие обработчики на document. Начиная с React 17, делегирование привязано к корневому контейнеру (root container). Это важно в проектах, где на одной странице живут несколько независимых React-приложений или гибрид React + legacy jQuery/vanilla.
Практический эффект:
- меньше неожиданных пересечений между независимыми root;
- проще изолировать поведение микрофронтендов;
- меньше «магии» на уровне глобального
document.
Но если у вас есть старый код с нативными listeners на document, порядок срабатывания может отличаться от того, что команда помнит по React 16.
Capture и Bubble в React на практике
React поддерживает обе фазы:
- capture:
onClickCapture; - bubble:
onClick.
function EventPhasesDemo() {
return (
<div
onClickCapture={() => console.log("parent capture")}
onClick={() => console.log("parent bubble")}
style={{ padding: 16, border: "1px solid #ccc" }}
>
<button
onClickCapture={() => console.log("button capture")}
onClick={() => console.log("button bubble")}
>
Нажми
</button>
</div>
);
}
Такой пример нужен не ради теории: в production через capture часто решают аналитику, трекинг и предвалидацию до основной бизнес-логики bubble-фазы.
Работа с Synthetic Event объектом
Раньше часто встречался совет вызывать event.persist(), чтобы сохранить событие для асинхронного использования. В актуальном React пуллинг событий удален, поэтому этот паттерн в большинстве случаев больше не нужен.
function InputLogger() {
const [value, setValue] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const nextValue = e.currentTarget.value;
setValue(nextValue);
setTimeout(() => {
// В React 17+ это безопасно без persist в типичном сценарии.
console.log("latest typed:", nextValue);
}, 300);
};
return <input value={value} onChange={handleChange} placeholder="Введите текст" />;
}
На практике лучше сохранять нужные данные сразу (nextValue), а не прокидывать весь event-объект глубоко в async-цепочки.
Таблица сравнения: Synthetic Events и альтернативы
| Критерий | Synthetic Events (React) | Native DOM events | Кастомный event bus |
|---|---|---|---|
| Кроссбраузерная унификация | Да | Частично, зависит от API | Нет, зависит от реализации |
| Интеграция с React-рендером | Полная | Ограниченная | Отсутствует |
| Делегирование по умолчанию | Да | Нужно организовывать вручную | Не относится к DOM |
| Контроль capture/bubble в дереве React | Да | Да, но в DOM-дереве | Нет нативной модели DOM-фаз |
| Риск конфликтов с React state | Низкий | Средний/высокий | Средний |
| Типичные кейсы | UI-события в компонентах | Интеграция с внешними SDK, document-level hooks | Доменные события между модулями |
| Главные ограничения | Нужно понимать границу с native listeners | Больше ручной рутины и cleanup | Легко создать второй источник истины |
Production pitfalls: где чаще всего ломается система событий
Ошибка 1. Смешивание React-обработчиков и native listeners без явной границы
Симптомы:
- обработчики срабатывают дважды;
stopPropagationне дает ожидаемого эффекта;- поведение отличается между экранами с разным монтажом root.
Последствие: нестабильный UX и трудно воспроизводимые баги.
Профилактика: разграничить зоны ответственности. Либо событие обрабатывается внутри React-дерева, либо на уровне native listeners с четкой причиной (например, интеграция SDK).
Ошибка 2. Отсутствие cleanup для нативных подписок
Симптомы:
- после переходов по роутам обработчик срабатывает многократно;
- растет потребление памяти;
- в логах видно дубли одинаковых действий.
Последствие: деградация производительности длинной сессии.
Профилактика: всегда удалять addEventListener в cleanup эффекта, где подписка создавалась.
Ошибка 3. Тяжелая логика прямо в onChange/onMouseMove
Симптомы:
- ввод текста «тормозит»;
- курсор мыши дергается на слабых устройствах;
- profiling показывает длинные обработчики событий.
Последствие: просадка отзывчивости даже при корректном batching.
Профилактика: отделять срочную UI-часть от тяжелой постобработки (startTransition, debounce, web worker, серверная фильтрация).
Ошибка 4. Неучтенные особенности portal и modal-слоев
Симптомы:
- клик в модалке закрывает фон не в том порядке;
- outside-click логика срабатывает неожиданно;
- аналитика получает лишние события.
Последствие: регрессии в критичных пользовательских сценариях.
Профилактика: проектировать события с учетом portal-структуры и явно тестировать capture/bubble последовательность.
Разбор производительности
Synthetic Events сами по себе редко являются узким местом. Чаще всего проблема заключается в том, что происходит после события:
- каскад
setStateбез декомпозиции дерева; - синхронная тяжелая фильтрация;
- лишние ререндеры из-за нестабильных props;
- дорогое DOM-измерение после каждого ввода/скролла.
Где система событий помогает:
- делегирование снижает количество DOM-listeners;
- единая модель упрощает оптимизацию и профилирование;
- лучше прогнозируется поведение в больших деревьях.
Где не помогает:
- если бизнес-логика в обработчике занимает десятки миллисекунд;
- если каждый event триггерит пересчет больших структур;
- если архитектура состояния провоцирует массовые ререндеры.
Практический ориентир:
- Профилируйте не только commit time, но и длительность самих обработчиков.
- На интенсивных событиях (
input,mousemove,scroll) измеряйте 95-й перцентиль задержки. - Делайте оптимизацию только после метрик на реальных устройствах.
Прокачай React за 7 дней
20 вопросов и разборов по React Hooks.
Когда Synthetic Events не закрывают задачу
Есть сценарии, где нужен прямой native API:
- Глобальные события вне React-root (например, legacy-виджет или интеграция CMS-скрипта).
- Специфичные браузерные события, которые удобнее обрабатывать на уровне
window/document. - Интеграция со сторонними библиотеками, которые уже управляют собственным event lifecycle.
В таких случаях лучше явно признать гибридную архитектуру, чем делать вид, что «все через React». Главное правило: один сценарий, один источник правды о событии.
Практики, которые работают в командах
- В ревью отмечайте слой обработки события: React-only или native-interop.
- Для каждого native listener фиксируйте причину и cleanup-стратегию в коде.
- Выносите сложные обработчики в отдельные функции/хуки, чтобы уменьшить шум JSX.
- Добавляйте автоматические тесты на порядок capture/bubble для критичных флоу.
- В observability собирайте метрики по частоте и длительности интерактивных обработчиков.
- Для рискованных изменений в event-логике делайте rollout через feature flag и заранее готовьте rollback-план.
Частые ошибки
- Полагаться на устаревшие советы про обязательный
event.persist(). - Считать, что
stopPropagationвсегда блокирует любые нативные обработчики вокруг React. - Держать тяжелую бизнес-логику прямо в
onChange. - Не учитывать, что модалки и dropdown часто рендерятся через portal.
- Смешивать два канала событий без документированной архитектуры.
Как отвечать на интервью про Synthetic Events
Рабочая структура ответа:
- Synthetic Event — это нормализованная обертка React над native DOM event.
- React использует делегирование и прогоняет capture/bubble по своему дереву обработчиков.
- После React 17 делегирование привязано к root container, а не к
document. event.persist()обычно больше не нужен, потому что пуллинг удален.- В production важно понимать границу между React-обработчиками и native listeners.
Такой ответ показывает, что вы знаете не только API, но и эксплуатационные риски.
Прокачай React для собеседований и production-задач
Разберем события, рендеринг и оптимизацию на реальных кейсах с персональной обратной связью.
Практический кейс из production: outside click для dropdown
Кейс: нужно закрывать dropdown по клику вне компонента. Команда часто делает только onBlur и получает баги с вложенными элементами, portal-рендером и мобильными устройствами.
Надежный вариант при гибридном подходе (React + native):
function UserMenu() {
const [open, setOpen] = useState(false);
const rootRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const handlePointerDown = (e: PointerEvent) => {
const target = e.target as Node | null;
if (!rootRef.current || !target) return;
if (!rootRef.current.contains(target)) {
setOpen(false);
}
};
document.addEventListener("pointerdown", handlePointerDown);
return () => document.removeEventListener("pointerdown", handlePointerDown);
}, []);
return (
<div ref={rootRef}>
<button onClick={() => setOpen((v) => !v)}>Меню</button>
{open && (
<ul>
<li>Профиль</li>
<li>Настройки</li>
<li>Выход</li>
</ul>
)}
</div>
);
}
Почему это production-safe:
- используется
pointerdown, чтобы закрытие происходило раньше цепочкиclick; - есть явный cleanup подписки;
- React-state остается единственным источником истины для UI.
Граница подхода: если dropdown рендерится в portal вне rootRef, проверку contains нужно проектировать с учетом container portal.
Чек-лист внедрения event-логики в существующий код
-
Где живет обработчик: React или native? Без этого шага команда почти всегда получает конфликт propagation.
-
Есть ли cleanup у подписок? Если нет cleanup, утечки и дубли обработчиков неизбежны.
-
Тяжелый ли обработчик? Если да, вынесите дорогую часть в отложенную/несрочную ветку.
-
Проверена ли работа с порталами и вложенными-компонентами? События в overlay-слоях ломаются в первую очередь.
-
Есть ли тесты на порядок срабатывания? Критичные сценарии должны покрывать capture/bubble и outside-click.
Этот чек-лист экономит много времени на регрессиях после рефакторинга интерактивных экранов.
FAQ
Synthetic Event полностью заменяет native event?
Нет. Он дает унифицированный интерфейс в React, но доступ к nativeEvent остается, и в интеграциях с внешним DOM-кодом нативный слой важен.
Нужно ли всегда писать onClickCapture?
Нет. Capture-фаза нужна точечно, когда важен приоритет обработки до bubble-логики.
Почему stopPropagation не всегда останавливает «все»?
Потому что часть обработчиков может находиться в отдельной нативной цепочке вне React, и на них React stopPropagation не распространяется автоматически.
Есть ли смысл использовать native listeners внутри React-приложения?
Да, но только по четкой причине: глобальные события, SDK-интеграции или специальные сценарии платформы.
Что важнее для производительности: Synthetic Events или оптимизация рендеров?
Обычно оптимизация рендеров и обработчиков важнее. Synthetic Events дают инфраструктурную базу, но не исправляют тяжелую бизнес-логику сами по себе.
Итоги
Synthetic Events в React решают практическую задачу: дают единый слой обработки событий и связывают интерактивность с системой рендера. Это снижает количество ошибок, если команда понимает границу между React и native DOM.
В production выигрывают команды, которые проектируют события как часть архитектуры: фиксируют слой ответственности, удаляют подписки, измеряют стоимость обработчиков и отдельно тестируют capture/bubble в критичных сценариях.
Если хотите углубиться дальше, полезно пройти разбор React Hooks для интервью, гид по memo/useMemo/useCallback и материал по error boundaries.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью.
Автор
Lexicon Team
Читайте также
frontend
Event Loop и React: как связаны и почему это влияет на рендер
Разбираем связь Event Loop и React без упрощений: macrotask, microtask, batching, commit, useEffect, scheduler, производительность и типичные ошибки на интервью.
frontend
20 задач по React с разбором: что реально проверяют на собеседовании и в production
Собрали 20 задач по React с разбором: ререндеры, keys, формы, refs, Suspense, Context, SSR, оптимизация и типичные ошибки, которые всплывают на интервью.
frontend
Webpack vs Vite для React: что выбрать в 2026 году и как объяснить выбор на интервью
Сравниваем Webpack и Vite для React: dev server, HMR, production build, экосистема, производительность, типичные ошибки и сильный ответ для собеседования.