Portals provide a way to render children into a DOM node that exists outside the DOM hierarchy of the parent component. This is particularly useful for modals, tooltips, and other elements that need to “break out” of their container.
1. Basic Portal Implementation
Creating a Portal
import React from 'react';
import ReactDOM from 'react-dom';
function Modal({ children }) {
return ReactDOM.createPortal(
<div className="modal">
{children}
</div>,
document.getElementById('modal-root')
);
}
// Usage
function App() {
const [showModal, setShowModal] = React.useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Show Modal</button>
{showModal && (
<Modal>
<h2>Modal Title</h2>
<p>Modal content goes here</p>
<button onClick={() => setShowModal(false)}>Close</button>
</Modal>
)}
</div>
);
}
HTML Setup
<!-- In your public/index.html -->
<div id="root"></div>
<div id="modal-root"></div>
2. Advanced Portal Techniques
Dynamic Portal Root Creation
function usePortal(id) {
const [portalRoot, setPortalRoot] = React.useState(null);
React.useEffect(() => {
let element = document.getElementById(id);
let created = false;
if (!element) {
element = document.createElement('div');
element.setAttribute('id', id);
document.body.appendChild(element);
created = true;
}
setPortalRoot(element);
return () => {
if (created && element.parentNode) {
element.parentNode.removeChild(element);
}
};
}, [id]);
return portalRoot;
}
function Portal({ id, children }) {
const target = usePortal(id);
return target ? ReactDOM.createPortal(children, target) : null;
}
Event Bubbling Through Portals
function ParentComponent() {
const handleClick = () => {
console.log('Clicked in parent, even though event originated in portal');
};
return (
<div onClick={handleClick}>
<p>Regular content</p>
<Modal>
<button>Click me (inside portal)</button>
</Modal>
</div>
);
}
3. Common Use Cases
Modal Dialogs
function ModalDialog({ isOpen, onClose, children }) {
if (!isOpen) return null;
return ReactDOM.createPortal(
<div className="modal-overlay">
<div className="modal-content">
<button className="modal-close" onClick={onClose}>×</button>
{children}
</div>
</div>,
document.getElementById('modal-root')
);
}
Tooltips
function Tooltip({ children, content }) {
const [isVisible, setIsVisible] = React.useState(false);
const triggerRef = React.useRef(null);
const [position, setPosition] = React.useState({ top: 0, left: 0 });
const updatePosition = () => {
if (triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX
});
}
};
React.useEffect(() => {
if (isVisible) {
updatePosition();
window.addEventListener('resize', updatePosition);
return () => window.removeEventListener('resize', updatePosition);
}
}, [isVisible]);
return (
<>
<span
ref={triggerRef}
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
>
{children}
</span>
{isVisible && ReactDOM.createPortal(
<div
className="tooltip"
style={{
position: 'absolute',
top: `${position.top}px`,
left: `${position.left}px`
}}
>
{content}
</div>,
document.body
)}
</>
);
}
Loading Indicators
function GlobalLoadingIndicator({ isLoading }) {
return isLoading ? ReactDOM.createPortal(
<div className="global-loader">
<div className="spinner"></div>
</div>,
document.getElementById('loading-root')
) : null;
}
4. Best Practices
- Accessibility:
- For modals, implement proper focus management
- Add ARIA attributes for screen readers
- Handle keyboard navigation (Escape to close, etc.)
- Performance:
- Reuse portal roots when possible
- Avoid frequent mounting/unmounting of portal content
- Consider lazy-loading portal content
- Styling:
- Use fixed or absolute positioning for overlay elements
- Ensure proper z-index stacking
- Handle body scroll locking for modals
- Testing:
test('portal renders correctly', () => {
const modalRoot = document.createElement('div');
modalRoot.setAttribute('id', 'modal-root');
document.body.appendChild(modalRoot);
render(<Modal>Test Content</Modal>);
expect(modalRoot).toHaveTextContent('Test Content');
document.body.removeChild(modalRoot);
});
5. Advanced Patterns
Context in Portals
const ThemeContext = React.createContext('light');
function ThemedPortal() {
return (
<ThemeContext.Provider value="dark">
<Modal>
<ThemeContext.Consumer>
{theme => <div className={`theme-${theme}`}>Content</div>}
</ThemeContext.Consumer>
</Modal>
</ThemeContext.Provider>
);
}
Nested Portals
function NestedPortals() {
return (
<Portal id="first-portal">
<div>First level portal content</div>
<Portal id="second-portal">
<div>Second level portal content</div>
</Portal>
</Portal>
);
}
Server-Side Rendering (SSR)
function SafePortal({ id, children }) {
const [isMounted, setIsMounted] = React.useState(false);
React.useEffect(() => {
setIsMounted(true);
}, []);
if (!isMounted) return null;
let portalRoot = document.getElementById(id);
if (!portalRoot) {
portalRoot = document.createElement('div');
portalRoot.setAttribute('id', id);
document.body.appendChild(portalRoot);
}
return ReactDOM.createPortal(children, portalRoot);
}
6. Troubleshooting
Common Issues and Solutions
- Portal content not appearing:
- Verify the target DOM node exists before rendering
- Check for typos in the target node ID
- Ensure the portal is mounted (especially with SSR)
- Event handling problems:
- Remember events bubble through the React tree, not the DOM tree
- Use
event.stopPropagation()
when needed
- Styling issues:
- Make sure portal content isn’t hidden by parent overflow
- Verify z-index values are appropriate
- Check for conflicting position values
Portals are a powerful feature that enable you to break out of the normal DOM hierarchy while maintaining React’s component structure and context. They’re particularly valuable for UI elements that need to visually “escape” their container constraints while remaining logically part of your React component tree.