What are portals (Portals) in React?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Medium
#React #Portals

Brief Answer

Portals are a mechanism in React that allows rendering child elements to a DOM node that exists outside the DOM hierarchy of the parent component.

Key portal features:

  • Allow rendering elements outside the parent component
  • Events bubble through the React tree, not the DOM tree
  • Useful for modals, tooltips, dropdown menus
  • Don’t break React context functionality

Common mistakes:

  • Incorrectly using portals for all components
  • Ignoring accessibility (a11y)
  • Improper portal cleanup

Key rule: Use portals only when you need to render an element outside the normal DOM flow! 🎯


Full Answer

Imagine you’re playing hide and seek. Usually children hide in their room, but portals are like a door to another room where you can hide, but still remain part of the game! 🚪

What are portals

Portals allow rendering child elements anywhere in the DOM tree:

// Creating a portal
import { createPortal } from 'react-dom';
 
function Modal({ children, isOpen }) {
  if (!isOpen) return null;
  
  // Render children to document.body, not the parent component
  return createPortal(
    <div className="modal">
      {children}
    </div>,
    document.body
  );
}

Why portals are needed

Portals solve CSS context and z-index problems:

// ❌ Problem without portals
function App() {
  return (
    <div style={{ position: 'relative', zIndex: 1 }}>
      <div style={{ overflow: 'hidden' }}>
        {/* Modal is clipped by container! */}
        <Modal>
          <h1>Modal window</h1>
        </Modal>
      </div>
    </div>
  );
}
 
// ✅ Solution with portals
function Modal({ children }) {
  return createPortal(
    <div className="modal-overlay">
      <div className="modal-content">
        {children}
      </div>
    </div>,
    document.body
  );
}

How portals work

Events and bubbling

Portals don’t change event behavior in React:

function ParentComponent() {
  const handleClick = () => {
    console.log('Click on parent');
  };
  
  return (
    <div onClick={handleClick}>
      <h1>Parent component</h1>
      
      {/* Event will bubble to ParentComponent, 
          even if button is in portal! */}
      {createPortal(
        <button>Button in portal</button>,
        document.body
      )}
    </div>
  );
}

Working with context

Portals maintain access to context:

const ThemeContext = createContext('light');
 
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <div>
        <Header />
        {/* Portal gets access to theme through context */}
        <PortalComponent />
      </div>
    </ThemeContext.Provider>
  );
}
 
function PortalComponent() {
  const theme = useContext(ThemeContext);
  
  return createPortal(
    <div className={`modal ${theme}`}>
      Modal window in theme: {theme}
    </div>,
    document.body
  );
}

Practical examples

1. Modal window

import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
 
function Modal({ isOpen, onClose, children }) {
  const [modalRoot, setModalRoot] = useState(null);
  
  useEffect(() => {
    // Create container for modal window
    const modalContainer = document.createElement('div');
    modalContainer.className = 'modal-root';
    document.body.appendChild(modalContainer);
    setModalRoot(modalContainer);
    
    // Cleanup
    return () => {
      document.body.removeChild(modalContainer);
    };
  }, []);
  
  if (!isOpen || !modalRoot) return null;
  
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        <button className="modal-close" onClick={onClose}>×</button>
        {children}
      </div>
    </div>,
    modalRoot
  );
}
 
// Usage
function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);
  
  return (
    <div>
      <button onClick={() => setIsModalOpen(true)}>
        Open modal window
      </button>
      
      <Modal 
        isOpen={isModalOpen} 
        onClose={() => setIsModalOpen(false)}
      >
        <h2>Modal window content</h2>
        <p>This modal renders outside the main DOM tree!</p>
      </Modal>
    </div>
  );
}

2. Tooltip

import { useState, useRef } from 'react';
import { createPortal } from 'react-dom';
 
function Tooltip({ children, content }) {
  const [isVisible, setIsVisible] = useState(false);
  const targetRef = useRef(null);
  const tooltipRef = useRef(null);
  
  const showTooltip = () => setIsVisible(true);
  const hideTooltip = () => setIsVisible(false);
  
  if (!isVisible) return (
    <span 
      ref={targetRef}
      onMouseEnter={showTooltip}
      onMouseLeave={hideTooltip}
    >
      {children}
    </span>
  );
  
  const targetRect = targetRef.current?.getBoundingClientRect();
  const position = targetRect ? {
    top: targetRect.bottom + window.scrollY + 5,
    left: targetRect.left + window.scrollX
  } : { top: 0, left: 0 };
  
  return createPortal(
    <div 
      ref={tooltipRef}
      className="tooltip"
      style={{
        position: 'absolute',
        top: `${position.top}px`,
        left: `${position.left}px`,
        zIndex: 1000
      }}
      onMouseEnter={showTooltip}
      onMouseLeave={hideTooltip}
    >
      {content}
    </div>,
    document.body
  );
}
 
// Usage
function App() {
  return (
    <div>
      <p>Hover over <Tooltip content="This is a tooltip!">this word</Tooltip> to see tooltip.</p>
    </div>
  );
}

When to use portals

1. Modal windows

// ✅ Ideal portal usage
function Modal({ isOpen, children }) {
  if (!isOpen) return null;
  
  return createPortal(
    <div className="modal-backdrop">
      <div className="modal-dialog">
        {children}
      </div>
    </div>,
    document.body
  );
}

2. Dropdown menus

// ✅ Dropdown menus outside container with overflow: hidden
function Dropdown({ options, isOpen }) {
  if (!isOpen) return null;
  
  return createPortal(
    <div className="dropdown-menu">
      {options.map(option => (
        <div key={option.value} className="dropdown-item">
          {option.label}
        </div>
      ))}
    </div>,
    document.body
  );
}

3. Notifications

// ✅ Notifications that should be on top of everything
function Notification({ message, isVisible }) {
  if (!isVisible) return null;
  
  return createPortal(
    <div className="notification toast">
      {message}
    </div>,
    document.getElementById('notification-root') || document.body
  );
}

Common mistakes

1. Improper cleanup

// ❌ Memory leak problem
function BadModal({ isOpen }) {
  if (!isOpen) return null;
  
  // Create new element on every render!
  const container = document.createElement('div');
  document.body.appendChild(container);
  
  return createPortal(<div>Modal window</div>, container);
}
 
// ✅ Proper cleanup
function GoodModal({ isOpen }) {
  const [container, setContainer] = useState(null);
  
  useEffect(() => {
    const div = document.createElement('div');
    document.body.appendChild(div);
    setContainer(div);
    
    return () => {
      // Cleanup on unmount
      document.body.removeChild(div);
    };
  }, []);
  
  if (!isOpen || !container) return null;
  
  return createPortal(<div>Modal window</div>, container);
}

2. Ignoring accessibility

// ❌ Inaccessible modal window
function BadModal({ isOpen }) {
  if (!isOpen) return null;
  
  return createPortal(
    <div>
      <h2>Header</h2>
      <p>Content</p>
    </div>,
    document.body
  );
}
 
// ✅ Accessible modal window
function GoodModal({ isOpen, onClose }) {
  const modalRef = useRef(null);
  
  useEffect(() => {
    if (isOpen && modalRef.current) {
      // Focus on modal window
      modalRef.current.focus();
      
      // Handle Escape
      const handleEscape = (e) => {
        if (e.key === 'Escape') onClose();
      };
      
      document.addEventListener('keydown', handleEscape);
      return () => document.removeEventListener('keydown', handleEscape);
    }
  }, [isOpen, onClose]);
  
  if (!isOpen) return null;
  
  return createPortal(
    <div 
      role="dialog" 
      aria-modal="true"
      ref={modalRef}
      tabIndex="-1"
    >
      <h2>Header</h2>
      <p>Content</p>
      <button onClick={onClose}>Close</button>
    </div>,
    document.body
  );
}

Best practices

1. Reusable portal hook

import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
 
function usePortal(containerId) {
  const [container, setContainer] = useState(null);
  
  useEffect(() => {
    // Try to find existing container
    let element = document.getElementById(containerId);
    
    // If none exists - create new one
    if (!element) {
      element = document.createElement('div');
      element.id = containerId;
      document.body.appendChild(element);
    }
    
    setContainer(element);
    
    return () => {
      // Remove only if we created it
      if (!document.getElementById(containerId)) {
        document.body.removeChild(element);
      }
    };
  }, [containerId]);
  
  return container;
}
 
// Using the hook
function Modal({ isOpen, children, containerId = 'modal-root' }) {
  const container = usePortal(containerId);
  
  if (!isOpen || !container) return null;
  
  return createPortal(children, container);
}

2. z-index management

// ✅ z-index system for portals
const Z_INDEX = {
  tooltip: 1000,
  dropdown: 1100,
  modal: 1200,
  notification: 1300
};
 
function Modal({ children }) {
  return createPortal(
    <div 
      className="modal-overlay"
      style={{ zIndex: Z_INDEX.modal }}
    >
      <div className="modal-content">
        {children}
      </div>
    </div>,
    document.body
  );
}

Summary

Portals are like a magic door that allows elements to appear in another place in the DOM! 🚪✨

  • Portals render elements outside the parent component hierarchy
  • Events bubble through the React tree, not DOM
  • Context is preserved when using portals

When to use:

  • Modal windows ✅
  • Tooltips and dropdown menus ✅
  • Notifications ✅

When not to use:

  • For regular components ❌
  • When you don’t need to exit parent context ❌

Practical tips:

  1. Always clean up portals
  2. Don’t forget about accessibility
  3. Use z-index system
  4. Create reusable hooks

Portals are a powerful tool for solving specific positioning problems in React! 💪


Want more useful React articles? Subscribe to EasyAdvice, bookmark the site and level up every day! 🚀