Event Bubbling Through Portals Explained0%

Event Bubbling Through Portals Explained

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

Event Bubbling Through Portals Explained

1. The Surprising Portal Event Behavior

One of the most fascinating aspects of React Portals is how events behave:

Even though a portal is mounted into a completely different DOM node in the physical HTML, events fired inside the portal bubble UP through the REACT COMPONENT TREE, NOT through the physical HTML DOM tree!

Consider this scenario:

Output
React Component Tree:
<ParentDiv onClick={handleParentClick}>
<ChildWithPortal>
└─ createPortal(<button>Click</button>, document.body)

In the physical HTML DOM:

  • The <button> is a direct child of <body>.
  • The <ParentDiv> is located deep inside <div id="root">.
  • Physically, <button> is NOT inside <ParentDiv>.

Yet, if you click the <button>: handleParentClick on <ParentDiv> WILL FIRE!

Output
Physical HTML DOM:
<body>
<div id="root"> ──> <ParentDiv> (Not an ancestor of button!) </div>
<button id="portal-btn">Click Me</button>
</body>
 
React Virtual DOM Event Flow:
[button clicked] ════(Bubbles through React Tree!)════► [ParentDiv handleParentClick fires!]

2. Why Does This Happen?

React does not rely on native DOM event listeners attached to individual elements.

As explored in our SyntheticEvent architecture lessons:

  • React attaches unified listeners at the root of the React tree (<div id="root">).
  • React's synthetic event dispatcher traverses the Virtual DOM component hierarchy, not the browser's physical DOM tree.
  • Because <ChildWithPortal> is a logical child of <ParentDiv> in the React tree, React bubbles the SyntheticEvent up through the component hierarchy as expected!

3. Practical Demonstration

JSX
import React, { useState } from 'react';
import { createPortal } from 'react-dom';
 
function PortalModal({ onClose }) {
return createPortal(
<div className="portal-dialog">
<p>Inside Portal (Physical child of document.body)</p>
<button className="btn-action">Action Button</button>
</div>,
document.body
);
}
 
export default function Container() {
const [clickCount, setClickCount] = useState(0);
 
// This handler will catch clicks from inside the portal!
const handleContainerClick = () => {
setClickCount(prev => prev + 1);
};
 
return (
<div className="parent-box" onClick={handleContainerClick}>
<h3>Parent Container (Clicks captured: {clickCount})</h3>
<p>Clicking the button inside the portal WILL increment this count!</p>
<PortalModal />
</div>
);
}

4. How to Prevent Unintended Bubbling

If your portal represents a modal or tooltip and you do not want its internal click events to trigger parent container handlers:

  • Call e.stopPropagation() inside your portal event handler!
JSX
// Stop event from bubbling up the React component tree:
<div onClick={(e) => e.stopPropagation()}>
{/* Children inside this container will NOT trigger parent handlers */}
</div>

Interactive Quiz

Practice Quiz

Question 1
How do SyntheticEvents bubble when an element is rendered through createPortal?
Question 2
If <Parent onClick={handleClick}> renders a portal that attaches a <button> to document.body, what happens when that button is clicked?
Question 3
Why does React implement event bubbling based on the React tree rather than the DOM tree?
Question 4
How can an engineer stop a portal's click events from reaching ancestor React components?
Question 5
If a native browser DOM event listener is attached via document.addEventListener('click'), will it receive portal clicks?
Next Lesson

Component Composition as an Alternative to Context

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext