Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Синтетичні події в React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**SyntheticEvent** - це крос-браузерна обгортка React навколо нативних DOM-подій. Вона нормалізує властивості на зразок `event.target.value` і використовує один кореневий слухач замість по одного на кожен елемент. ```jsx function handleClick(e) { e.preventDefault(); // однаково в будь-якому браузері console.log(e.type); // "click" console.log(e.nativeEvent); // сирий DOM-об'єкт якщо потрібен } ``` **Ключове:** React 17 прибрав пулінг подій, тому асинхронний доступ до властивостей події працює без виклику `event.persist()`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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-об'єкт браузера, коли він потрібен сторонній бібліотеці. ### Швидкий приклад ```javascript 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 обнуляв усі властивості і повертав об'єкт у пул для повторного використання. Це непомітно ламало асинхронний код: ```javascript // React 16 - ламається function handleClick(e) { setTimeout(() => { console.log(e.target); // null - об'єкт вже переробили }, 0); } ``` Виправлення - `e.persist()`, який виймав подію з пулу: ```javascript // React 16 - виправлено function handleClick(e) { e.persist(); setTimeout(() => { console.log(e.target); // <button> }, 0); } ``` React 17 прибрав пулінг повністю. Тепер події - звичайні об'єкти. `persist()` залишився як no-op для зворотної сумісності. Я достатньо разів бачив цей async-null баг у реальних логах, щоб тепер завжди зчитувати значення подій синхронно. Безпечний патерн для обох версій: ```javascript function handleClick(e) { const value = e.target.value; // зчитуємо синхронно setTimeout(() => console.log(value), 0); // завжди працює } ``` ### event.target проти event.currentTarget Уяви кнопку зі span всередині: ```javascript <button onClick={handle}> <span>Клік сюди</span> </button> ``` Юзер натиснув на `<span>`. Тепер `e.target` - це `<span>`, а `e.currentTarget` - це `<button>`. Обробник на кнопці, але клік потрапив на span. Для форм завжди читай з `e.currentTarget`, якщо потрібен елемент з обробником, а не той на який натиснули. ### Типові помилки **Читання властивостей події в асинхронному коді без попереднього захоплення.** ```javascript // Ламалось у 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-елементів.** ```javascript // Не роби так - обходить делегування, може спрацювати двічі, тече пам'ять 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`. ## Приклади ### Базовий: скасування навігації ```javascript 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>`, незалежно від того який дочірній елемент всередині нього натиснули. ### Середній рівень: асинхронна відправка форми ```javascript 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 синхронно і зберігає в стані. // Асинхронний обробник взагалі не торкається об'єкта події. ``` ### Просунутий: фаза захоплення ```javascript 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 на будь-якому пропсі події. // Корисно для перехоплення події до того як вона дістанеться дочірнього обробника. ``` Обробники з захопленням спрацьовують зверху вниз до того як елемент отримує подію. Це дозволяє батьківському компоненту перехопити і скасувати подію до того як будь-який дочірній компонент її оброблятиме.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.