Back to articles

React Portals: Ownership, Placement, and the Real Tree

React Portals: Ownership, Placement, and the Real Tree

1. Motivation: Something Felt Off

I didn’t start thinking about React portals because I needed a modal. I started thinking about them because something about how they worked felt… off.

A component can render somewhere else in the DOM, yet React still treats it as part of its original parent. State, context, and event handling just work. That contradiction is a hint: the DOM isn’t React’s mental model. React has its own tree — the Fiber tree — and the DOM is just a projection.

If you’ve ever used portals and assumed they were “just for modals,” that’s understandable. But understanding portals isn’t about building UI patterns; it’s about understanding how React separates ownership from placement, and why that separation matters for reconciliation, context, and events.

So if the DOM isn’t the source of truth, what is? That’s where Fiber comes in.


2. How This Used to Be Done (Pre-Portals)

Before portals, developers had to rely on hacks. Want a modal or tooltip rendered outside its parent? You’d manually append it to document.body and manage its lifecycle with refs. Event handling had to be wired explicitly, and context was out of the question — anything that relied on parent ownership was lost.

// Pre-portal modal
class Modal extends React.Component {
  componentDidMount() {
    document.body.appendChild(this.el);
  }

  componentWillUnmount() {
    document.body.removeChild(this.el);
  }

  render() {
    return ReactDOM.createPortal(
      <div className="modal">{this.props.children}</div>,
      this.el
    );
  }
}

It worked, but it was brittle. Lose a cleanup step, and your app breaks. Event handlers or state references outside the modal? Forget it. You were fighting the DOM and React simultaneously.

These hacks worked, but they also exposed a deeper truth: React’s mental model and the DOM were never meant to align perfectly. That tension begged for a better approach.


3. Enter Portals: The Concept

Portals act like a bridge. The component is still owned by its logical parent in React’s tree, but it’s rendered somewhere else in the DOM. Ownership and placement are now separate.

Think of it like a puppet: the puppet performs on a distant stage (DOM node), but the puppeteer (parent component) still controls every move — strings, context, and state. The audience sees it somewhere else, but in the puppeteer’s mind, nothing has moved.

// Modern Portal Example
function Modal({ children }) {
  return ReactDOM.createPortal(
    <div className="modal">{children}</div>,
    document.getElementById("modal-root")
  );
}

It works seamlessly. But why? To understand that, we need to look behind the curtain: the Fiber tree.


4. Fiber Tree vs DOM Tree

React doesn’t operate on the DOM. It operates on its Fiber tree — a virtual representation of the UI that tracks every component, its state, and its relationships. The DOM is merely an output target, like a canvas. React decides what goes where, when, and how, independently of the browser’s hierarchy.

Fiber Tree:
App
 ├─ Parent
 │   └─ Modal (logical child)

DOM Tree:
<body>
 ├─ #root
 │   └─ Parent
 └─ #modal-root
     └─ Modal (rendered here)

Portals expose this subtle but crucial truth: React’s reconciliation, event propagation, and context traversal happen in Fiber, not in the DOM.


5. Event Bubbling Across Portals

One of the first “magic” moments with portals is realizing that events still bubble naturally to the parent. Even though the DOM says the modal is elsewhere, clicks, keyboard events, and synthetic events flow through the Fiber tree as if it were in place.

function Parent() {
  function handleClick() {
    console.log("Parent clicked!");
  }

  return (
    <div onClick={handleClick}>
      <Modal>
        <button>Click me</button>
      </Modal>
    </div>
  );
}

Click the button inside the modal — the parent’s handler triggers. The puppet’s strings work across the stage. That’s React’s synthetic event system following Fiber, not the DOM.


6. Context and State Preservation

Ownership matters more than placement. Even if the modal is physically outside the parent, it still consumes parent context and preserves state correctly. This wouldn’t be possible if React relied on the DOM for hierarchy.

const ThemeContext = React.createContext("light");

function Parent() {
  return (
    <ThemeContext.Provider value="dark">
      <Modal>
        <Child />
      </Modal>
    </ThemeContext.Provider>
  );
}

function Child() {
  const theme = React.useContext(ThemeContext); // "dark"
  return <div>{theme}</div>;
}

Portals are not a UI trick — they reveal how React’s mental model separates ownership from placement.


7. Pitfalls and Subtle Gotchas

Portals are not magical. DOM quirks still apply: z-index stacking, focus management, and SSR can trip you up. If you forget that the DOM still enforces visual stacking, you’ll see odd rendering behavior.

Even the best abstractions have limits. Portals solve ownership vs. placement, but they don’t replace good architectural decisions or accessibility considerations.


8. Why This Matters

Portals expose the gap between what the DOM shows and what React knows. Understanding this gap helps you reason about context, event propagation, and component design in a deeper, more predictable way.

What looks like a minor feature — a modal rendered elsewhere — is actually a window into React’s inner workings, a way to see the separation of mental model vs physical representation.


9. Conclusion: Reflecting on Portals

Portals are more than a convenience. They’re a lesson in ownership, state, and React’s real tree. When you use them, remember: the DOM is just a stage. The Fiber tree — the mental model React keeps — is the real home.

Next time a component lives somewhere else in the DOM, remember: in Fiber’s eyes, it never left.