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:
❌ Common mistakes:
Key rule: Use portals only when you need to render an element outside the normal DOM flow! 🎯
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! 🚪
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
);
}
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
);
}
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>
);
}
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
);
}
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>
);
}
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>
);
}
// ✅ 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
);
}
// ✅ 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
);
}
// ✅ 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
);
}
// ❌ 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);
}
// ❌ 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
);
}
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);
}
// ✅ 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
);
}
Portals are like a magic door that allows elements to appear in another place in the DOM! 🚪✨
When to use:
When not to use:
Practical tips:
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! 🚀