В данной статье рассматриваются особенности, которые связаны с обработкой нативных событий (native events) в React-приложениях. Существует проблема частичной потери контекста функционального компонента при обработке нативных событий, которые навешиваются на элементы с помощью глобальных объектов document, window или через ссылки (refs). В статье рассматривается данная проблема и предлагается способ её решения (один из вариантов).

Мотивация
Основной мотивацией для написании данной статьи стал мой практический опыт разработки React-приложений и моих личных ошибок при обработке нативных событий, из которых следовало не стандартное поведение приложения проявляющееся в неправильном выводе данных на экран, отправки неправильных запросов на сервер и во многом другом.
Я достаточно часто сталкиваюсь с необходимостью обрабатывать нативные события и до определённого времени делал это не правильно, что приводило к ошибкам. На удивление я нашёл не много информации, которая помогает разобраться с такими ошибками и ещё меньше - объясняющие их природу и почему всё работает так, а не иначе.
Эта статья призвана повысить осведомлённость читателя о существовании некоторых проблем, которые могут возникнуть при обработке нативных событий, и показать необходимость в учитывании их особенностей.
Используемая версия React.js, Node.js и других зависимостей
Версия React.js - 18.2.0.
Версия Node.js - 16.20.2.
Подробный список dependencies тестового проекта:
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
}Синтетические и нативные события
Для начала разберёмся что вообще такое синтетическое событие в контексте React.js.
Синтетическое событие - это кроссбраузерная обёртка SyntheticEvent, которая предназначена для унификации работы с событиями в DOM-дереве.
Синтетические события являются крайне полезными при разработке веб-приложений, которые ориентированы на множество браузеров (поддержка кроссбраузерности). Благодаря им гарантируется (насколько это возможно), что ваше веб-приложение будет одинаково обрабатывать события в Opera, Google, Safari, Mozilla Firefox, Yandex и других популярных браузерах. Также они позволяют избежать утечек памяти благодаря автоматическому управлению памятью (чего нет при обработке нативных событий), что делает синтетические события ещё более полезными.
Какие события можно считать синтетическими? В общем-то все, которые поддерживаются на данный момент и содержатся (важно) в определении DOM-дерева функционального или классового компонента (в JSX-коде).
То есть, все события, которые обрабатываются в следующем программном коде, являются синтетическими:
// ...
return (
<>
<div
className="wrapper"
onMouseLeave={(e) => {
// Обработка синтетического события ...
}}
>
<div
id="containerId"
className="container"
onDoubleClick={(e) => {
// Обработка синтетического события ...
}}
>
<span
onClick={(e) => {
// Обработка синтетического события ...
}}
>{value}</span>
</div>
</div>
</>
);
// ...Это также не трудно понять по объекту event, который возвращается внутрь функции обработчика синтетических событий. Как правило данный объект всегда имеет класс SyntheticBaseEvent.
Пояснения по объекту event
Объект event - это первый параметр, который возвращается в функцию-обработчик какого-либо события (нативного, синтетического или, даже, кастомного).
Вот код, который описывает получение и вывод этого объекта на экран (на выходе получим объект класса SyntheticBaseEvent):
// ...
<div className="wrapper">
<div
id="containerId"
className="container"
onClick={(e) => {
// Вывод объекта класса SyntheticBaseEvent
console.log(e);
}}
>
<span>{value}</span>
</div>
</div>
// ...В обработчик обычно передаётся более короткая форма параметра event - e.

С синтетическими событиями разобрались. Что же такое нативные события?
В контексте данной статьи к нативным событиям относятся все события, которые не являются синтетическими, но при этом связаны с обработкой события в DOM-дереве.
Вообще, я думаю, что чистых нативных событий сейчас уже нет, поскольку каждый браузер может обрабатывать их по своему и модифицировать цепочку прототипов объекта event как угодно. В каком-то браузере может быть параметр, который не поддерживается в другом (пример из вакуума не лишённый смысла). В общем, что считать чистым нативным событием в контексте React - вопрос философский и я предлагаю читателю поразмышлять на данную тему.
Разграничение между синтетическими событиями и нативными было дано не просто так, поскольку есть разница в обработке этих событий и об этой разнице будет сказано чуть позже.
Нативные события, в основном, обрабатываются с помощью стандартного метода addEventListener, который вызывается у глобальных объектов window, document или не пустых (not null) ссылок (refs).
Вот пример обработки нативных событий:
// ...
useEffect(() => {
const container = document.getElementById("containerId");
// Навешиваем обработчики на нативные события
container.addEventListener("click", mouseMoveHandler);
window.addEventListener("mousedown", mouseDownHandler);
return () => {
// Удаляем обработчики с нативных событий,
// иначе они будут работать и после выхода со страницы
window.removeEventListener("mousedown", mouseDownHandler);
// Кроме данной строки - здесь можно и не удалять,
// ведь если нет DOM-элемента, то и нет проблемы :)
container.removeEventListener("click", mouseMoveHandler);
};
}, []);
// ...В общем-то с разницей между нативными (в контексте данной статьи) и синтетическими событиями разобрались.
Кстати, стоит упомянуть о том, что при обработке синтетических событий можно получить объект event, который мы бы получили при обработке нативного события. Для этого можно обратиться к атрибуту nativeEvent в объекте event.
Проблема при обработке нативного события
Далее следует ввести понятие контекста React-компонента.
Контекст React-компонента (в рамках данной статьи) - это все функции, состояния, refs, переменные и константы, которые определены в рамках одного React-компонента.
Например, контекстом React-компонента (далее - контекст или контекст компонента) является всё, что определено в следующем программном коде:
// ...
const [value, setValue] = useState(0);
const [state, setState] = useState({
arr: []
});
const timer = useRef(null);
const handler = (e) => {
console.log(e);
}
// ...или:
// ...
constructor(props) {
super(props);
this.state = {
value: 0
};
this.timer = React.createRef(null);
this.mouseMoveHandler = this.mouseMoveHandler.bind(this);
}
// ...В общем, можно воспринимать контекст, как окружение компонента. В классовом компоненте его роль принимает объект this.
Теперь перейдём к проблеме.
В контексте компонента очень часто разработчики определяют и используют состояния (useState, this.state), которые играют если не ключевую роль, то очень важную при функционировании и разработке пользовательского интерфейса.
Не редко требуется, чтобы при обработке синтетических событий мы использовали значение каких-нибудь состояний. Это может быть нужно для чего угодно. Например, требуется по щелчку на кнопку мыши в условный блок сохранять значение, который сгенерировал рандомайзер.

