React Portal
React Portal is a feature that allows you to render children into a DOM node that exists outside the parent component’s DOM hierarchy.
Table of Contents
- React Portal
What is React Portal?
React Portal allows you to render React components into DOM nodes that are outside the normal component hierarchy, while maintaining the React context and event handling.
Key Concepts:
- Render outside hierarchy: Components render in different DOM locations
- Maintain React context: Props, state, and events work normally
- Accessibility: Better for modals, tooltips, and overlays
- Event bubbling: Events bubble up through React tree, not DOM tree
Problem Portal Solves:
// Without Portal: Modal renders inside parent container
function App() {
return (
<div className="app" style={{ overflow: 'hidden' }}>
<h1>My App</h1>
<Modal /> {/* Modal is constrained by parent's overflow: hidden */}
</div>
);
}
// With Portal: Modal renders at document.body level
function App() {
return (
<div className="app" style={{ overflow: 'hidden' }}>
<h1>My App</h1>
<Modal /> {/* Modal renders outside parent constraints */}
</div>
);
}
Portal Syntax
1. Basic Portal:
import ReactDOM from 'react-dom';
function Portal({ children }) {
return ReactDOM.createPortal(
children,
document.body
);
}
// Usage
function App() {
return (
<div>
<h1>Main Content</h1>
<Portal>
<div>This renders at document.body</div>
</Portal>
</div>
);
}
2. Portal with Custom Container:
function CustomPortal({ children, containerId }) {
const [container, setContainer] = useState(null);
useEffect(() => {
let element = document.getElementById(containerId);
if (!element) {
element = document.createElement('div');
element.id = containerId;
document.body.appendChild(element);
}
setContainer(element);
return () => {
if (element && element.parentNode) {
element.parentNode.removeChild(element);
}
};
}, [containerId]);
return container ? ReactDOM.createPortal(children, container) : null;
}
// Usage
function App() {
return (
<div>
<h1>Main Content</h1>
<CustomPortal containerId="modal-root">
<div>Custom portal content</div>
</CustomPortal>
</div>
);
}
3. Portal with Event Handling:
function EventPortal({ children, onClose }) {
const handleClick = (e) => {
// Event bubbles up through React tree, not DOM tree
if (e.target === e.currentTarget) {
onClose();
}
};
return ReactDOM.createPortal(
<div onClick={handleClick} className="portal-overlay">
{children}
</div>,
document.body
);
}
// Usage
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Show Modal</button>
{showModal && (
<EventPortal onClose={() => setShowModal(false)}>
<div className="modal-content">
<h2>Modal Content</h2>
<p>Click outside to close</p>
</div>
</EventPortal>
)}
</div>
);
}
Common Use Cases
1. Modal Dialogs:
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return ReactDOM.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>,
document.body
);
}
// Usage
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div className="app">
<button onClick={() => setIsModalOpen(true)}>
Open Modal
</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h2>Modal Title</h2>
<p>This modal renders outside the app container</p>
<button onClick={() => setIsModalOpen(false)}>Close</button>
</Modal>
</div>
);
}
2. Tooltips:
function Tooltip({ children, content, position = 'top' }) {
const [isVisible, setIsVisible] = useState(false);
const [tooltipStyle, setTooltipStyle] = useState({});
const triggerRef = useRef();
const showTooltip = () => {
if (triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect();
setTooltipStyle({
position: 'fixed',
left: rect.left + rect.width / 2,
top: position === 'top' ? rect.top - 10 : rect.bottom + 10,
transform: 'translateX(-50%)'
});
}
setIsVisible(true);
};
const hideTooltip = () => {
setIsVisible(false);
};
return (
<>
<span
ref={triggerRef}
onMouseEnter={showTooltip}
onMouseLeave={hideTooltip}
>
{children}
</span>
{isVisible && ReactDOM.createPortal(
<div className="tooltip" style={tooltipStyle}>
{content}
</div>,
document.body
)}
</>
);
}
// Usage
function App() {
return (
<div>
<Tooltip content="This is a tooltip">
<button>Hover me</button>
</Tooltip>
</div>
);
}
3. Notifications:
function NotificationSystem() {
const [notifications, setNotifications] = useState([]);
const addNotification = (message, type = 'info') => {
const id = Date.now();
setNotifications(prev => [...prev, { id, message, type }]);
// Auto remove after 5 seconds
setTimeout(() => {
removeNotification(id);
}, 5000);
};
const removeNotification = (id) => {
setNotifications(prev => prev.filter(n => n.id !== id));
};
return (
<>
<button onClick={() => addNotification('Success message!', 'success')}>
Show Success
</button>
{ReactDOM.createPortal(
<div className="notification-container">
{notifications.map(notification => (
<div
key={notification.id}
className={`notification ${notification.type}`}
onClick={() => removeNotification(notification.id)}
>
{notification.message}
</div>
))}
</div>,
document.body
)}
</>
);
}
4. Loading Overlays:
function LoadingOverlay({ isLoading, children }) {
return (
<>
{children}
{isLoading && ReactDOM.createPortal(
<div className="loading-overlay">
<div className="loading-spinner">Loading...</div>
</div>,
document.body
)}
</>
);
}
// Usage
function App() {
const [loading, setLoading] = useState(false);
const handleAsyncOperation = async () => {
setLoading(true);
await new Promise(resolve => setTimeout(resolve, 2000));
setLoading(false);
};
return (
<LoadingOverlay isLoading={loading}>
<div className="app-content">
<h1>My App</h1>
<button onClick={handleAsyncOperation}>
Start Loading
</button>
</div>
</LoadingOverlay>
);
}
Advanced Portal Patterns
1. Portal with Context:
const PortalContext = React.createContext();
function PortalProvider({ children }) {
const [portals, setPortals] = useState([]);
const addPortal = (id, content) => {
setPortals(prev => [...prev, { id, content }]);
};
const removePortal = (id) => {
setPortals(prev => prev.filter(p => p.id !== id));
};
return (
<PortalContext.Provider value={{ addPortal, removePortal }}>
{children}
{portals.map(portal => (
<React.Fragment key={portal.id}>
{ReactDOM.createPortal(portal.content, document.body)}
</React.Fragment>
))}
</PortalContext.Provider>
);
}
function usePortal() {
const context = useContext(PortalContext);
if (!context) {
throw new Error('usePortal must be used within PortalProvider');
}
return context;
}
// Usage
function App() {
return (
<PortalProvider>
<MainContent />
</PortalProvider>
);
}
function MainContent() {
const { addPortal, removePortal } = usePortal();
const showModal = () => {
addPortal('modal', (
<div className="modal">
<h2>Modal Content</h2>
<button onClick={() => removePortal('modal')}>Close</button>
</div>
));
};
return (
<div>
<button onClick={showModal}>Show Modal</button>
</div>
);
}
2. Portal with Focus Management:
function FocusPortal({ children, onClose }) {
const portalRef = useRef();
useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
// Focus the portal
if (portalRef.current) {
portalRef.current.focus();
}
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [onClose]);
return ReactDOM.createPortal(
<div
ref={portalRef}
tabIndex={-1}
className="focus-portal"
role="dialog"
aria-modal="true"
>
{children}
</div>,
document.body
);
}
3. Portal with Animation:
function AnimatedPortal({ children, isVisible, onClose }) {
const [isRendered, setIsRendered] = useState(false);
useEffect(() => {
if (isVisible) {
setIsRendered(true);
} else {
const timer = setTimeout(() => setIsRendered(false), 300);
return () => clearTimeout(timer);
}
}, [isVisible]);
if (!isRendered) return null;
return ReactDOM.createPortal(
<div
className={`animated-portal ${isVisible ? 'visible' : 'hidden'}`}
onClick={onClose}
>
<div onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
document.body
);
}
// CSS for animation
const styles = `
.animated-portal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.3s ease;
}
.animated-portal.visible {
opacity: 1;
}
.animated-portal.hidden {
opacity: 0;
}
`;
4. Portal with Multiple Containers:
function MultiPortal({ children, container = 'body' }) {
const [targetContainer, setTargetContainer] = useState(null);
useEffect(() => {
let element;
if (container === 'body') {
element = document.body;
} else {
element = document.getElementById(container);
if (!element) {
element = document.createElement('div');
element.id = container;
document.body.appendChild(element);
}
}
setTargetContainer(element);
}, [container]);
if (!targetContainer) return null;
return ReactDOM.createPortal(children, targetContainer);
}
// Usage
function App() {
return (
<div>
<MultiPortal container="modal-root">
<div>Modal content</div>
</MultiPortal>
<MultiPortal container="tooltip-root">
<div>Tooltip content</div>
</MultiPortal>
</div>
);
}
Best Practices
1. Clean Up Portals:
// Good: Clean up portal containers
function GoodPortal({ children, containerId }) {
useEffect(() => {
const container = document.getElementById(containerId);
return () => {
if (container && container.children.length === 0) {
container.remove();
}
};
}, [containerId]);
return ReactDOM.createPortal(children, document.getElementById(containerId));
}
// Bad: Don't leave orphaned containers
function BadPortal({ children }) {
return ReactDOM.createPortal(children, document.body);
// No cleanup
}
2. Handle Portal Container Creation:
// Good: Create container if it doesn't exist
function SafePortal({ children, containerId }) {
const [container, setContainer] = useState(null);
useEffect(() => {
let element = document.getElementById(containerId);
if (!element) {
element = document.createElement('div');
element.id = containerId;
document.body.appendChild(element);
}
setContainer(element);
}, [containerId]);
return container ? ReactDOM.createPortal(children, container) : null;
}
3. Manage Focus and Accessibility:
// Good: Handle focus management
function AccessiblePortal({ children, onClose }) {
const portalRef = useRef();
useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
// Focus the portal
if (portalRef.current) {
portalRef.current.focus();
}
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [onClose]);
return ReactDOM.createPortal(
<div
ref={portalRef}
tabIndex={-1}
role="dialog"
aria-modal="true"
>
{children}
</div>,
document.body
);
}
4. Use Portals for Overlays:
// Good: Use portal for modals and overlays
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={onClose}>
<div onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>,
document.body
);
}
// Bad: Don't use portal for regular content
function BadPortal({ children }) {
return ReactDOM.createPortal(
<div>{children}</div>,
document.body
);
// Regular content should stay in component hierarchy
}
Common Interview Questions
Q: What is React Portal?
- A feature that allows rendering React components into DOM nodes outside the parent component’s hierarchy.
Q: When would you use React Portal?
- For modals, tooltips, notifications, and other overlays that need to render outside parent containers.
Q: How do you create a React Portal?
- Use
ReactDOM.createPortal(children, container)where container is a DOM node.
Q: Do events bubble through portals?
- Events bubble up through the React component tree, not the DOM tree, even with portals.
Q: How do you handle cleanup with portals?
- Remove portal containers when they’re no longer needed and clean up event listeners.
Q: What are the benefits of using portals?
- Better accessibility, avoiding CSS overflow issues, and cleaner component structure.
Q: How do you manage focus with portals?
- Use refs to focus portal elements and handle keyboard events like Escape key.
Q: Can you use context with portals?
- Yes, portals maintain React context even when rendering outside the component tree.
Q: How do you handle multiple portals?
- Use different container IDs or create a portal management system with context.
Q: What’s the difference between portal and regular rendering?
- Portals render outside the parent DOM hierarchy while maintaining React functionality.
Summary
- React Portal allows rendering components outside the normal DOM hierarchy
- Common use cases include modals, tooltips, notifications, and overlays
- Event bubbling works through React tree, not DOM tree
- Context and props work normally with portals
- Cleanup is important to prevent memory leaks
- Accessibility should be considered with focus management
- Best practices include proper container management and cleanup
- Understanding portals is crucial for building overlays and complex UI components
Interview angle
- “What is a portal for?” - rendering into a DOM node outside the parent, while staying in the React tree. Modals, tooltips and dropdowns need it to escape a parent’s
overflow: hidden,transformorz-indexstacking context. - “Does an event inside a portal bubble to the React parent?” - yes. Bubbling follows the React tree, not the DOM tree, which surprises people and is usually what you want - a click inside a modal still reaches the handler that logically contains it.
- “What do you still have to handle yourself?” - accessibility: focus trapping, restoring focus on close,
aria-modal, and Escape to dismiss. A portal only solves placement.<dialog>and headless UI libraries give the rest.