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".
// 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) // ✅
}React does not attach onClick to each DOM element — it delegates through a single root-level listener. When an event bubbles up, React wraps it in a SyntheticEvent (cross-browser wrapper) and dispatches it through the component tree.
Important React 17 change:
| React ≤ 16 | React 17+ | |
|---|---|---|
| Listener attached to | document | root container (managed by createRoot) |
| Multiple React apps on one page | ❌ Conflict (shared document listener) | ✅ Each app has its own listener |
stopPropagation() from non-React code | Breaks React event flow | Only affects within that root |
| Event pooling | ✅ Reused event object, needed e.persist() for async | ❌ Pooling removed — events safe to use async |
Why the change: safer when embedding React into legacy apps (gradual upgrade), embedding micro-frontends, or running two React apps on the same page — their listeners no longer clash.
// Pre-React 17: needed persist for async access
function onClick(e) {
e.persist() // ❌ not needed from React 17
setTimeout(() => console.log(e.target), 100)
}
// React 17+: just works
function onClick(e) {
setTimeout(() => console.log(e.target), 100) // ✅
}