React không attach onClick vào từng DOM element — delegate qua một listener duy nhất ở root. Khi event bubble lên, React chuyển thành SyntheticEvent (wrapper cross-browser) rồi dispatch xuống component tree.
Thay đổi quan trọng React 17:
| React ≤ 16 | React 17+ | |
|---|---|---|
| Listener gắn vào | document | root container (do createRoot quản lý) |
| Multi React app trên cùng page | ❌ Conflict (chung document listener) | ✅ Mỗi app có listener riêng |
stopPropagation() từ non-React code | Phá React event flow | Chỉ ảnh hưởng trong root đó |
| Event pooling | ✅ Reuse event object, cần e.persist() cho async | ❌ Bỏ pooling — event dùng async thoải mái |
Vì sao thay đổi: safer khi nhúng React vào app cũ (gradual upgrade), nhúng micro-frontend, hoặc 2 React apps cùng page — listener không "đánh nhau".
jsx
// Pre-React 17: cần persist nếu dùng async
function onClick(e) {
e.persist() // ❌ không cần từ React 17
setTimeout(() => console.log(e.target), 100)
}
// React 17+: dùng thoải mái
function onClick(e) {
setTimeout(() => console.log(e.target), 100) // ✅
}