Синтетичні події в React
SyntheticEvent - це крос-браузерна обгортка React навколо нативних DOM-подій. Вона нормалізує властивості на зразок event.target.value так, щоб вони однаково працювали в Chrome, Firefox та старому IE, і використовує один кореневий слухач замість по одного на кожен елемент.
Теорія
TL;DR
- Нативні події в різних браузерах мають трохи різні властивості. SyntheticEvent дає один стабільний API поверх усього цього.
- React чіпляє ОДИН слухач до
#root, а не по одному на кожен елемент. Це і є делегування подій. - У React 16 об'єкти подій повторно використовувались після завершення обробника, і асинхронний доступ до
event.targetповертавnull. React 17 прибрав цю поведінку. e.target- елемент, що спричинив подію.e.currentTarget- елемент де знаходиться обробник.e.nativeEventповертає сирий DOM-об'єкт браузера, коли він потрібен сторонній бібліотеці.
Швидкий приклад
function Button() {
const handleClick = (e) => {
console.log(e.type); // "click" - нормалізовано
console.log(e.nativeEvent); // MouseEvent - сирий DOM-об'єкт
e.preventDefault(); // однаково в будь-якому браузері
};
return <a href="/somewhere" onClick={handleClick}>Клік</a>;
}
// Скасовує навігацію, виводить "click" без браузерних відмінностейe який React передає в обробник - не нативний MouseEvent. Це обгортка над ним. Та сама форма, ті самі методи, плюс e.nativeEvent щоб дістатися оригіналу.
Як працює делегування подій
React не чіпляє окремий слухач до кожної кнопки, інпуту чи посилання. Він реєструє один слухач на кореневому DOM-вузлі (#root у React 17+, document у React 16). Нативні події спливають по DOM як завжди. React перехоплює їх у корені, огортає в SyntheticEvent, нормалізує властивості і доставляє до потрібного обробника.
Список з 1000 кнопок все одно матиме рівно один слухач у браузері. Динамічні списки обробляють кліки без жодного перепідписування. Перехід від document до #root у React 17 важливий при змішуванні версій React на одній сторінці. Кожне React-дерево тепер ізольоване.
Пулінг подій: React 16 проти React 17
У React 16 об'єкти SyntheticEvent були в пулі. Після завершення обробника React обнуляв усі властивості і повертав об'єкт у пул для повторного використання. Це непомітно ламало асинхронний код:
// React 16 - ламається
function handleClick(e) {
setTimeout(() => {
console.log(e.target); // null - об'єкт вже переробили
}, 0);
}Виправлення - e.persist(), який виймав подію з пулу:
// React 16 - виправлено
function handleClick(e) {
e.persist();
setTimeout(() => {
console.log(e.target); // <button>
}, 0);
}React 17 прибрав пулінг повністю. Тепер події - звичайні об'єкти. persist() залишився як no-op для зворотної сумісності. Я достатньо разів бачив цей async-null баг у реальних логах, щоб тепер завжди зчитувати значення подій синхронно. Безпечний патерн для обох версій:
function handleClick(e) {
const value = e.target.value; // зчитуємо синхронно
setTimeout(() => console.log(value), 0); // завжди працює
}event.target проти event.currentTarget
Уяви кнопку зі span всередині:
<button onClick={handle}>
<span>Клік сюди</span>
</button>Юзер натиснув на <span>. Тепер e.target - це <span>, а e.currentTarget - це <button>. Обробник на кнопці, але клік потрапив на span. Для форм завжди читай з e.currentTarget, якщо потрібен елемент з обробником, а не той на який натиснули.
Типові помилки
Читання властивостей події в асинхронному коді без попереднього захоплення.
// Ламалось у React 16, працює у React 17+ але залишається нечітким
const handleChange = (e) => {
setTimeout(() => setState(e.target.value), 0);
};
// Явно і безпечно скрізь
const handleChange = (e) => {
const val = e.target.value;
setTimeout(() => setState(val), 0);
};Використання addEventListener у useEffect для React-елементів.
// Не роби так - обходить делегування, може спрацювати двічі, тече пам'ять
useEffect(() => {
document.getElementById('btn').addEventListener('click', handler);
});
// Роби так
<button onClick={handler}>Клік</button>addEventListener у useEffect доречний для подій на window - resize, scroll - де немає відповідного React-пропсу. Але завжди повертай функцію очищення.
Повернення false для скасування дефолтної дії - як у звичайному HTML. У React return false з обробника нічого не робить. Потрібно явно викликати e.preventDefault().
Плутанина між e.target і e.currentTarget у обробниках форм. Якщо поле форми містить вкладені елементи, e.target може вказувати на дочірній span. Для елемента з обробником завжди використовуй e.currentTarget.
Де зустрічається
- React-компоненти - кожен
onClick,onChange,onSubmitотримує SyntheticEvent за замовчуванням - Next.js - обробники форм в app router працюють так само прозоро
- Material UI - SyntheticEvent проходить з компонентів бібліотеки у твої обробники без змін
- Інтеграція з D3.js - використовуй
e.nativeEventколи D3 очікує сирий DOM-об'єкт - TypeScript - імпортуй
MouseEvent,ChangeEvent,FormEventзreact, не з DOM-типів
Питання на співбесіді
Q: Яка різниця між event.target та event.currentTarget?
A: target - елемент що отримав подію, тобто найглибший з натиснутих. currentTarget - елемент де прикріплений обробник. Різняться коли натиснутий елемент є дочірнім відносно елемента з обробником.
Q: Чому React 17 переніс слухачі подій з document на #root?
A: Прикріплення до document спричиняло конфлікти при роботі двох версій React на одній сторінці або при змішуванні з іншими фреймворками. Перенос на кореневий вузол ізолює кожне React-дерево.
Q: Чи потрібен event.persist() у React 17+?
A: Ні. Пулінг прибрали, тому persist() нічого не робить. У старих кодових базах зустрічається - безпечно видалити, залишати теж не шкідливо.
Q: Як stopPropagation() працює всередині синтетичної системи React?
A: Виклик e.stopPropagation() виставляє внутрішній прапор у циклі dispatch React. React припиняє виклик батьківських обробників. Нативна подія все одно досягає кореня де сидить єдиний слухач, але далі React її не диспатчить.
Q (senior): Як SyntheticEvent поводиться з порталами?
A: Події з елементів у порталі спливають крізь React-дерево компонентів, а не по DOM-дереву. Клік всередині модального вікна-порталу спрацює на React-батьку, навіть якщо DOM порталу знаходиться за межами того вузла. Для отримання реального DOM-шляху використовуй e.nativeEvent.
Приклади
Базовий: скасування навігації
function NavLink() {
const handleClick = (e) => {
e.preventDefault(); // зупиняє навігацію браузера
console.log(e.type); // "click"
console.log(e.currentTarget.href); // href посилання
};
return <a href="/dashboard" onClick={handleClick}>Дашборд</a>;
}
// Виводить "click" і href, перезавантаження немаєe.currentTarget тут завжди буде тегом <a>, незалежно від того який дочірній елемент всередині нього натиснули.
Середній рівень: асинхронна відправка форми
function PaymentForm({ onSubmit }) {
const [card, setCard] = useState('');
const handleChange = (e) => {
setCard(e.target.value); // синхронне читання - безпечно в будь-якій версії
};
const handleSubmit = async (e) => {
e.preventDefault();
// card береться зі стану, а не з об'єкта події
// тому проблем з async немає
const res = await fetch('/charge', {
method: 'POST',
body: JSON.stringify({ card }),
});
onSubmit(await res.json());
};
return (
<form onSubmit={handleSubmit}>
<input value={card} onChange={handleChange} placeholder="Номер картки" />
<button type="submit">Оплатити</button>
</form>
);
}
// handleChange зчитує event.target.value синхронно і зберігає в стані.
// Асинхронний обробник взагалі не торкається об'єкта події.Просунутий: фаза захоплення
function CaptureDemo() {
return (
<div
onClickCapture={() => console.log('1. div capture')}
onClick={() => console.log('4. div bubble')}
>
<button
onClickCapture={() => console.log('2. button capture')}
onClick={() => console.log('3. button bubble')}
>
Клік
</button>
</div>
);
}
// Результат при натисканні кнопки:
// 1. div capture
// 2. button capture
// 3. button bubble
// 4. div bubble
//
// React підтримує фазу захоплення через суфікс *Capture на будь-якому пропсі події.
// Корисно для перехоплення події до того як вона дістанеться дочірнього обробника.Обробники з захопленням спрацьовують зверху вниз до того як елемент отримує подію. Це дозволяє батьківському компоненту перехопити і скасувати подію до того як будь-який дочірній компонент її оброблятиме.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.