React Portals
Introduction
React applications typically render components within a hierarchical structure, where child components are nested inside their parent components in the DOM. However, there are scenarios where you might want to render a component outside of its parent hierarchy while maintaining the React context and event system. This is where React Portals come in.
React Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component. This feature is particularly useful for components like modals, tooltips, and floating menus that need to visually "break out" of their containers.
What are React Portals?
A portal is created using React's createPortal
API, which takes two arguments:
- The child elements (React component or elements) to render
- A DOM element where these children should be rendered
The syntax looks like this:
import { createPortal } from 'react-dom';
function MyComponent() {
return createPortal(
childrenToRender,
domNode
);
}
Despite being rendered in a different DOM location, the portal still behaves like a normal React child in all other ways. Context, event bubbling, and other React features work exactly as if the portal wasn't being used.
When to Use React Portals
React Portals are particularly useful in situations where:
- CSS Limitations: When parent components have CSS properties like
overflow: hidden
orz-index
that would visually clip or hide child elements - DOM Structure: When you need to render content outside the flow of the document, like modals or tooltips
- Third-party DOM Containers: When working with external DOM containers not managed by your React app
Creating a Basic Portal
Let's create a simple portal example. First, make sure you have a DOM node to render your portal content into:
<!-- in your index.html -->
<div id="root"></div>
<div id="portal-root"></div>
Now, let's create a component that uses a portal:
import React from 'react';
import { createPortal } from 'react-dom';
function PortalExample() {
// This element will be rendered to the "portal-root" div
return createPortal(
<div className="portal">
<h2>This content is rendered through a portal!</h2>
</div>,
document.getElementById('portal-root')
);
}
function App() {
return (
<div className="app">
<h1>React Portals Example</h1>
<div className="container">
<p>This content is inside the main React tree</p>
<PortalExample />
</div>
</div>
);
}
In this example, even though the PortalExample
component is rendered within the App
component in the component tree, the actual DOM rendering happens in the separate portal-root
div.
Creating a Modal with Portals
A common use case for portals is creating modals that sit above your page content. Let's build a reusable Modal component:
import React from 'react';
import { createPortal } from 'react-dom';
import './Modal.css'; // We'll create this file next
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay">
<div className="modal-container">
<button className="modal-close-button" onClick={onClose}>
×
</button>
<div className="modal-content">
{children}
</div>
</div>
</div>,
document.getElementById('portal-root')
);
}
To style our modal, let's create a Modal.css
file:
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-container {
background-color: white;
padding: 20px;
border-radius: 4px;
max-width: 500px;
width: 100%;
position: relative;
}
.modal-close-button {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
.modal-content {
margin-top: 20px;
}
Now, let's use our Modal component:
import React, { useState } from 'react';
import Modal from './Modal';
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div className="app">
<h1>React Portals Modal Example</h1>
<button onClick={() => setIsModalOpen(true)}>
Open Modal
</button>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
>
<h2>Hello from the Modal!</h2>
<p>This content is rendered using a React Portal.</p>
<p>Although the Modal component is part of the App component tree,
it's rendered into a different DOM node.</p>
<button onClick={() => setIsModalOpen(false)}>Close</button>
</Modal>
</div>
);
}
Event Bubbling Through Portals
One important feature of portals is that events still bubble up through the React component hierarchy, regardless of the actual DOM structure. This means that events fired from inside a portal will propagate to ancestors in the containing React tree, even if those elements are not ancestors in the DOM.
Here's an example to demonstrate:
import React, { useState } from 'react';
import { createPortal } from 'react-dom';
function EventBubblingDemo() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log('Button clicked inside portal');
};
return (
<div
className="parent"
onClick={() => console.log('Parent div clicked')}
>
<h2>Event Bubbling Through Portals Example</h2>
<p>Button click count: {count}</p>
{createPortal(
<div className="portal-content">
<button onClick={handleClick}>
Click me (Inside Portal)
</button>
</div>,
document.getElementById('portal-root')
)}
</div>
);
}
When you click the button inside the portal:
- The button's click handler runs (
handleClick
) - The count state updates
- The "Button clicked inside portal" message appears in the console
- The event bubbles up to the parent div in the React tree
- The "Parent div clicked" message appears in the console
Practical Use Cases for React Portals
1. Tooltips
Tooltips often need to be positioned relative to their trigger but shouldn't be affected by parent container styles:
import React, { useState, useRef } from 'react';
import { createPortal } from 'react-dom';
function Tooltip({ children, content }) {
const [isVisible, setIsVisible] = useState(false);
const triggerRef = useRef(null);
const [position, setPosition] = useState({ top: 0, left: 0 });
const showTooltip = () => {
const rect = triggerRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX
});
setIsVisible(true);
};
const hideTooltip = () => {
setIsVisible(false);
};
return (
<>
<span
ref={triggerRef}
onMouseEnter={showTooltip}
onMouseLeave={hideTooltip}
>
{children}
</span>
{isVisible && createPortal(
<div
className="tooltip"
style={{
position: 'absolute',
top: `${position.top}px`,
left: `${position.left}px`,
background: 'black',
color: 'white',
padding: '5px',
borderRadius: '3px',
zIndex: 1000
}}
>
{content}
</div>,
document.body
)}
</>
);
}
2. Floating Notifications/Toasts
Notification systems that display temporary messages often use portals:
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
// First, create a ToastContainer component that will be our portal target
function ToastContainer({ children }) {
return createPortal(
<div className="toast-container">
{children}
</div>,
document.getElementById('portal-root')
);
}
// Then create an individual Toast component
function Toast({ message, type = 'info', duration = 3000, onClose }) {
useEffect(() => {
const timer = setTimeout(() => {
onClose();
}, duration);
return () => clearTimeout(timer);
}, [duration, onClose]);
return (
<div className={`toast toast-${type}`}>
{message}
</div>
);
}
// Finally create a Toast manager component
function ToastManager() {
const [toasts, setToasts] = useState([]);
const addToast = (message, type = 'info', duration = 3000) => {
const id = Date.now();
setToasts(prev => [...prev, { id, message, type, duration }]);
};
const removeToast = (id) => {
setToasts(prev => prev.filter(toast => toast.id !== id));
};
return (
<>
<button onClick={() => addToast('Operation successful!', 'success')}>
Show Success Toast
</button>
<button onClick={() => addToast('Something went wrong!', 'error')}>
Show Error Toast
</button>
<ToastContainer>
{toasts.map(toast => (
<Toast
key={toast.id}
message={toast.message}
type={toast.type}
duration={toast.duration}
onClose={() => removeToast(toast.id)}
/>
))}
</ToastContainer>
</>
);
}
Best Practices for Using Portals
- Use portals sparingly: They're a powerful tool but add complexity to your app architecture
- Clean up portal nodes: Make sure to handle component unmounting properly
- Keep accessibility in mind: Ensure that portaled content is still accessible
- Maintain event bubbling: Remember that events bubble through React's virtual DOM, not the actual DOM
- Manage focus: For UI elements like modals, manage keyboard focus correctly for accessibility
Common Pitfalls with Portals
1. Missing DOM Container
If the DOM container element doesn't exist when the portal tries to render, you'll get an error. Always ensure your container exists before rendering the portal.
function SafePortal({ children, containerId }) {
const [portalContainer, setPortalContainer] = useState(null);
useEffect(() => {
const container = document.getElementById(containerId);
setPortalContainer(container);
}, [containerId]);
if (!portalContainer) return null;
return createPortal(children, portalContainer);
}
2. Server-Side Rendering Compatibility
Portals require DOM APIs that aren't available during server-side rendering. For SSR compatibility, you need to conditionally create portals only on the client:
import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
function SSRSafePortal({ children, selector }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return mounted ?
createPortal(children, document.querySelector(selector)) :
null;
}
Summary
React Portals provide a powerful way to render components outside their parent DOM hierarchy while maintaining the React component tree relationship. They're especially useful for components like modals, tooltips, and floating menus that need to break out of container boundaries.
Key points to remember:
- Portals are created with
createPortal(child, container)
- Events bubble through the React component tree, not the DOM tree
- Portals are useful for rendering content that should visually "break out" of its container
- Common use cases include modals, tooltips, floating menus, and notifications
Additional Resources
To learn more about React Portals, check out these resources:
Exercises
- Create a modal component using portals that can be reused across your application.
- Build a tooltip component that uses portals to ensure it isn't clipped by parent containers.
- Implement a global notification system using portals that can display messages from any component.
- Create a dropdown menu that uses portals to break out of its container when there's not enough space.
Now that you understand React Portals, you can create more sophisticated UI components that aren't constrained by the DOM hierarchy.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)