Unleashing the Power of React Portals for Flexible UI Components
Master advanced UI patterns with the unexpected flexibility of React Portals
Jul 03, 2024 - 12:35 • 6 min read
Unleashing the Power of React Portals for Flexible UI Components
React Portals are one of those advanced but often overlooked features of React. This post dives deep into the concept of React Portals, showing how to use them effectively to build flexible UI components that transcend the ordinary.
React Portals provide a first-class way to render children into a DOM node that exists outside the parent component's DOM hierarchy.
What are React Portals?
By default, React components render children into their DOM hierarchy. However, there are times when you need to render a child into a different DOM node, like creating a modal, tooltip, or any other UI element that should break out of the parent container.
This is where React Portals come into play.
Here's a simple example of a React Portal:
import React from 'react';
import ReactDOM from 'react-dom';
function MyPortal({ children }) {
return ReactDOM.createPortal(
children,
document.getElementById('portal-root')
);
}
In this snippet, ReactDOM.createPortal
accepts two arguments:
- The first is the JSX elements to render.
- The second is the DOM node where you want to render those elements.
Creating a Simple Modal with React Portals
Let's enhance the previous example to build a simple modal component using React Portals.
Step 1: Setup the HTML structure
Make sure your index.html
file has the following structure:
<div id="root"></div>
<div id="portal-root"></div>
Step 2: Create the Modal Component
Here, we create a Modal
component using React Portals:
import React from 'react';
import ReactDOM from 'react-dom';
import './Modal.css'; // Assuming you have some CSS for the modal
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return ReactDOM.createPortal(
<div className="modal-overlay">
<div className="modal">
<button onClick={onClose} className="modal-close">×</button>
{children}
</div>
</div>,
document.getElementById('portal-root')
);
}
export default Modal;
In this Modal
component:
- We check if
isOpen
is true. If not, we returnnull
to avoid rendering anything. - If
isOpen
is true, we useReactDOM.createPortal
to render the modal content inside theportal-root
element.
Step 3: Using the Modal Component
Now, let's use the Modal
component in our application.
import React, { useState } from 'react';
import Modal from './Modal';
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div className="App">
<button onClick={() => setIsModalOpen(true)}>Open Modal</button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h1>Hello from the Modal!</h1>
</Modal>
</div>
);
}
export default App;
In this App component:
- We manage the state of the modal using
useState
hook. - When the button is clicked, we set
isModalOpen
to true, making the modal visible. - We pass
isModalOpen
andonClose
handler to theModal
component.
This approach keeps our modal logic and structure clean and separate from the rest of the application, adhering to React's declarative nature.
Enhancing the Modal
Closing the Modal on Click Outside
To enhance the UX, let's add a feature where clicking outside the modal closes it.
Update the Modal
component:
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
const handleOutsideClick = (e) => {
if (e.target.className === 'modal-overlay') {
onClose();
}
};
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={handleOutsideClick}>
<div className="modal">
<button onClick={onClose} className="modal-close">×</button>
{children}
</div>
</div>,
document.getElementById('portal-root')
);
}
Now, when the user clicks outside the modal (modal-overlay
), the onClose
function is called, closing the modal.
Accessibility Considerations
Accessibility (a11y) must be a priority for any web application. To make our modal more accessible, let's add some ARIA attributes.
Update the Modal
component again:
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
const handleOutsideClick = (e) => {
if (e.target.className === 'modal-overlay') {
onClose();
}
};
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={handleOutsideClick} role="dialog" aria-modal="true">
<div className="modal">
<button onClick={onClose} className="modal-close" aria-label="Close Modal">×</button>
{children}
</div>
</div>,
document.getElementById('portal-root')
);
}
- We added
role="dialog"
to themodal-overlay
. - We added
aria-modal="true"
to themodal-overlay
. - We added
aria-label="Close Modal"
to the close button.
These changes make it clearer to screen readers that this is a dialog (modal) and what its purpose is.
Code Splitting with Dynamic Imports
While not exclusive to Portals, code-splitting is a performance optimization technique that pairs well with them. Often, modals can be a secondary part of the user experience and may not need to be loaded upfront. Instead, you can dynamically import them when needed.
Here's how you can leverage dynamic imports with React Portals:
Dynamic Import of Modal
import React, { useState, Suspense, lazy } from 'react';
const Modal = lazy(() => import('./Modal'));
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div className="App">
<button onClick={() => setIsModalOpen(true)}>Open Modal</button>
<Suspense fallback={<div>Loading...</div>}>
{isModalOpen && (
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<h1>Hello from the Modal!</h1>
</Modal>
)}
</Suspense>
</div>
);
}
export default App;
By using React.lazy
and Suspense
, we load the modal component only when it's needed. This improves the initial load time of our application.
Portals for Tooltips and Dropdowns
While modals are a common use case for portals, they are not the only one. Tooltips and dropdowns also benefit greatly from portals, as they often need to break out of their parent's stacking context.
Tooltip Example
Let's create a simple tooltip component using portals.
import React from 'react';
import ReactDOM from 'react-dom';
import './Tooltip.css';
function Tooltip({ text, targetRef }) {
const { top, left, height } = targetRef.current.getBoundingClientRect();
return ReactDOM.createPortal(
<div className="tooltip" style={{ top: top + height + window.scrollY, left: left + window.scrollX }}>
{text}
</div>,
document.body
);
}
export default Tooltip;
In this Tooltip
component:
- We get the position of the target element using
getBoundingClientRect
. - We calculate the position for the tooltip and use
ReactDOM.createPortal
to render it insidedocument.body
.
Using the Tooltip Component
import React, { useRef, useState } from 'react';
import Tooltip from './Tooltip';
function App() {
const buttonRef = useRef(null);
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
return (
<div className="App">
<button
ref={buttonRef}
onMouseEnter={() => setIsTooltipVisible(true)}
onMouseLeave={() => setIsTooltipVisible(false)}
>
Hover me
</button>
{isTooltipVisible && <Tooltip text="This is a tooltip" targetRef={buttonRef} />}
</div>
);
}
export default App;
In this App
component:
- We manage the visibility of the tooltip using
useState
. - We use
useRef
to get the position of the button element and pass it to theTooltip
component.
This keeps the tooltip logic simple and detached from its parent, rendering it where it naturally belongs in the DOM hierarchy.
Conclusion
React Portals provide a powerful way to build flexible and detached UI components that cannot be achieved easily with traditional React rendering techniques. By mastering React Portals, you can create sophisticated UIs with modals, tooltips, dropdowns, and any other components that need to render outside their parent DOM hierarchy.
Experiment with Portals and combine them with other React features like React.lazy
and Suspense
for performance optimizations. And always keep accessibility in mind to ensure your applications are usable by all.
Happy coding!