What are React Portals and when would you use them?
4 minintermediatereactportals
Quick Answer
React Portals provide a way to render children into a DOM node that exists outside the parent component's DOM hierarchy. This is useful for modals, tooltips, dropdowns, and other UI elements that need to break out of their container's styling constraints.
Detailed Answer
What are React Portals and when would you use them?
Answer:
React Portals provide a way to render children into a DOM node that exists outside the parent component's DOM hierarchy. This is useful for modals, tooltips, dropdowns, and other UI elements that need to break out of their container's styling constraints.
Basic Portal Implementation:
import { createPortal } from 'react-dom';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
function Modal({ isOpen, onClose, children }: ModalProps) {
if (!isOpen) 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>,
document.body // Portal target
);
}
// Usage
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<button onClick={() => setIsModalOpen(true)}>
Open Modal
</button>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
>
<h2>Modal Title</h2>
<p>Modal content goes here...</p>
</Modal>
</div>
);
}
Advanced Portal with Dynamic Container:
function Portal({ children, containerId = 'portal-root' }: {
children: React.ReactNode;
containerId?: string;
}) {
const [container, setContainer] = useState<HTMLElement | null>(null);
useEffect(() => {
// Find or create portal container
let portalContainer = document.getElementById(containerId);
if (!portalContainer) {
portalContainer = document.createElement('div');
portalContainer.id = containerId;
document.body.appendChild(portalContainer);
}
setContainer(portalContainer);
// Cleanup
return () => {
if (portalContainer && portalContainer.parentNode) {
portalContainer.parentNode.removeChild(portalContainer);
}
};
}, [containerId]);
if (!container) return null;
return createPortal(children, container);
}
// Usage
function Tooltip({ children, content }: { children: React.ReactNode; content: string }) {
const [isVisible, setIsVisible] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 });
const triggerRef = useRef<HTMLDivElement>(null);
const updatePosition = useCallback(() => {
if (triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + window.scrollY + 8,
left: rect.left + window.scrollX
});
}
}, []);
return (
<>
<div
ref={triggerRef}
onMouseEnter={() => {
updatePosition();
setIsVisible(true);
}}
onMouseLeave={() => setIsVisible(false)}
>
{children}
</div>
<Portal>
{isVisible && (
<div
style={{
position: 'absolute',
top: position.top,
left: position.left,
background: 'black',
color: 'white',
padding: '4px 8px',
borderRadius: '4px',
zIndex: 1000,
pointerEvents: 'none'
}}
>
{content}
</div>
)}
</Portal>
</>
);
}
Portal with Focus Management:
function AccessibleModal({ isOpen, onClose, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const previousActiveElement = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
// Store the currently focused element
previousActiveElement.current = document.activeElement as HTMLElement;
// Focus the modal
modalRef.current?.focus();
// Prevent body scroll
document.body.style.overflow = 'hidden';
// Handle escape key
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
// Restore focus
previousActiveElement.current?.focus();
};
}
}, [isOpen, onClose]);
if (!isOpen) return null;
return createPortal(
<div
className="modal-overlay"
onClick={onClose}
role="dialog"
aria-modal="true"
>
<div
ref={modalRef}
className="modal-content"
onClick={(e) => e.stopPropagation()}
tabIndex={-1}
>
{children}
</div>
</div>,
document.body
);
}
When to Use Portals:
- Modals and Dialogs - Break out of parent container styling
- Tooltips and Popovers - Position relative to viewport
- Dropdowns - Avoid z-index and overflow issues
- Notifications - Render at app level
- Loading Overlays - Cover entire application
Portal Best Practices:
- Always clean up - Remove portal containers when unmounting
- Handle focus management - For accessibility
- Use proper z-index - Ensure portals appear above other content
- Consider SSR - Portals don't work during server-side rendering
- Test with screen readers - Ensure accessibility
// Custom hook for portal management
function usePortal(containerId: string) {
const [container, setContainer] = useState<HTMLElement | null>(null);
useEffect(() => {
let portalContainer = document.getElementById(containerId);
if (!portalContainer) {
portalContainer = document.createElement('div');
portalContainer.id = containerId;
document.body.appendChild(portalContainer);
}
setContainer(portalContainer);
return () => {
if (portalContainer?.parentNode) {
portalContainer.parentNode.removeChild(portalContainer);
}
};
}, [containerId]);
const Portal = useCallback(({ children }: { children: React.ReactNode }) => {
if (!container) return null;
return createPortal(children, container);
}, [container]);
return Portal;
}
// Usage
function MyComponent() {
const Portal = usePortal('my-portal');
return (
<div>
<Portal>
<div>This renders in a portal!</div>
</Portal>
</div>
);
}