Portals

Rendering components to a different DOM node with react-dom's createPortal -- modals, event bubbling behavior, and setting up a portal target, with simple examples.

Advanced 12 min
TR

Portals

The last topic in the Advanced React category -- a way to render a component to a different DOM node than its position in the React tree: Portals.

What Is a Portal? Getting Started with createPortal

react-dom's createPortal function lets us render a component to a different place in the DOM:

import { createPortal } from "react-dom";

function BasicPortalExample() {
  // createPortal(child, container), `child`'ı normal React ağacındaki
  // YERİNE değil, DOM'daki farklı bir düğüme (`container`) render eder --
  // burada `document.body`'nin kendisine. Component ağacında (React
  // DevTools'ta) hâlâ BasicPortalExample'ın İÇİNDE görünür, ama gerçek
  // DOM'da tamamen farklı bir yerdedir.
  return createPortal(<p className="tooltip">I'm rendered directly on body!</p>, document.body);
}

createPortal(child, container) renders child into the container DOM node, instead of its normal position in the React tree. It still appears in its expected place in the component tree (in React DevTools), but its actual DOM position is completely different.

Using Portals for Modals

The most common use case for Portals is modals:

import { useState } from "react";
import { createPortal } from "react-dom";

function Modal({ onClose, children }) {
  // Bir modal, Portal'ın en yaygın kullanım alanıdır -- modal'ın CSS'i
  // (position: fixed, z-index) sayfanın geri kalanının ÜSTÜNDE görünmesini
  // sağlar, ama gerçek DOM konumu ("bir kartın içinde" gibi) bunu bazen
  // engelleyebilir (overflow: hidden gibi). Portal, modal'ı doğrudan
  // `document.body`'ye render ederek bu sorunu ORTADAN KALDIRIR.
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(event) => event.stopPropagation()}>
        {children}
        <button onClick={onClose}>Close</button>
      </div>
    </div>,
    document.body,
  );
}

function ModalWithPortalExample() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      {isOpen && (
        <Modal onClose={() => setIsOpen(false)}>
          <p>This is a modal, rendered outside the normal DOM tree.</p>
        </Modal>
      )}
    </div>
  );
}

A modal's CSS (position: fixed, a high z-index) needs to make it appear ABOVE the rest of the page -- but the modal's actual DOM position (say, inside a card with overflow: hidden) can sometimes prevent that. A Portal ELIMINATES this problem by rendering the modal directly into document.body.

Event Bubbling: A Portal's Surprising Behavior

The most important (and most surprising) property of Portals is how events behave:

import { useState } from "react";
import { createPortal } from "react-dom";

function Popup() {
  return createPortal(<button>Click me (rendered in document.body)</button>, document.body);
}

function EventBubblingThroughPortalExample() {
  const [clicks, setClicks] = useState(0);

  return (
    // ÖNEMLİ: Popup, DOM'da bu <div>'in DIŞINDA (document.body'de)
    // render ediliyor. Ama içindeki <button>'a tıklandığında, onClick
    // yine de BURADA (React ağacındaki gerçek konumunda) çalışır --
    // React, event'leri gerçek DOM ağacına göre değil, KENDİ component
    // ağacına göre "bubble" ettirir. Bu, Portal'ların en şaşırtıcı ama en
    // kullanışlı özelliği.
    <div onClick={() => setClicks(clicks + 1)}>
      <p>Clicks: {clicks}</p>
      <Popup />
    </div>
  );
}

Popup renders OUTSIDE the outer <div> in the DOM (into document.body). But clicking the button inside it still causes onClick to bubble up to the outer <div> -- React propagates events according to its OWN component tree, not the actual DOM tree. This is the most important behavior to know when using Portals.

Setting Up a Portal Target

Instead of document.body, a dedicated target is usually used:

import { createPortal } from "react-dom";

function Tooltip({ text }) {
  // document.body yerine, index.html'de özel olarak ayrılmış bir hedef
  // kullanmak daha yaygındır -- örneğin <div id="tooltip-root"></div>,
  // uygulamanın #root'una KARDEŞ (sibling) olarak eklenir. Bu, portal
  // içeriğinin kendi stillerini/konumunu yönetmesini kolaylaştırır.
  const target = document.getElementById("tooltip-root");

  if (!target) {
    return null;
  }

  return createPortal(<span className="tooltip">{text}</span>, target);
}

function PortalTargetSetupExample() {
  return (
    <div>
      <p>Hover for more info</p>
      <Tooltip text="This tooltip lives in its own DOM node." />
    </div>
  );
}

Adding something like <div id="tooltip-root"></div> as a SIBLING to the app's #root in index.html is common practice -- it makes it easier for the portal's content to manage its own styles and positioning.

Summary and Glossary

createPortal(child, container) renders a component to a different DOM node while KEEPING its position in the React tree -- the most common uses are modals, tooltips, and dropdowns (to avoid CSS properties like overflow: hidden on ancestor elements). Events bubble according to React's component tree, not the actual DOM position -- this lets us keep using Portals like normal components.

Glossary

Portal — A mechanism for rendering a component to a different DOM node while preserving its position in the React tree.

Event Bubbling — An event propagating upward from the element it was triggered on, through its ancestor elements.

Practical Project

There's a real, runnable example project that brings together the concepts from this category (React Performance, Error Boundaries, Lazy Loading & Code Splitting, Suspense, Portals): Advanced React Demo -- an application showing a course list optimized with React.memo, an Error Boundary, a detail panel code-split with React.lazy + Suspense, and a Portal modal, all working together.

You can download it and run it yourself, and read through the code line by line:

git clone https://github.com/cdurgun/react-course-projects.git
cd react-course-projects
npm install
cd projects/advanced-react
npm run dev

The react-course-projects repo uses npm workspaces -- npm install only needs to run once, at the repo root, and every project folder shares the same dependencies (no separate node_modules per folder). If you've already run npm install at the root, you can just cd react-course-projects/projects/advanced-react and run npm run dev.