React Portal Utility
react-portal simplifies the creation and management of React Portals, enabling developers to render children into a different part of the DOM tree, outside the parent component's hierarchy. This is particularly useful for modals, tooltips, lightboxes, and notifications that require specific positioning or need to break out of CSS `overflow: hidden` containers. The current stable version is 4.3.0. It leverages React's official Portal API (introduced in React 16) to provide a robust solution. A key differentiator is its dual component approach, offering both a low-level `<Portal />` for maximum control and a `<PortalWithState />` for common stateful interactions (e.g., close on ESC, close on outside click) without external dependencies. Notably, since v4.1.0, it includes a fallback mechanism to support React v15 while primarily targeting React v16 and newer, making it flexible for diverse project ecosystems. It focuses on clean markup, SSR compatibility, and minimalistic design. The project demonstrates an active maintenance cadence, with recent minor releases addressing bugs and ensuring compatibility.
Common errors
-
TypeError: ReactDOM.createPortal is not a function
cause Attempting to use `react-portal` v4.0.0 with a React version older than 16.0.0.fixUpgrade `react` and `react-dom` to at least version 16.0.0. If you must support React 15, upgrade `react-portal` to v4.1.0 or later for its fallback mechanism. -
ReferenceError: document is not defined
cause Accessing the `document` object or rendering `react-portal` components directly in a Server-Side Rendering (SSR) environment without client-side checks.fixEnsure `Portal` components are only rendered on the client-side, or use conditional rendering based on `typeof document !== 'undefined'` for any custom logic that interacts with the DOM. -
Objects are not valid as a React child (found: object with keys {openPortal, closePortal, isOpen, portal}). If you meant to render a collection of children, use an array instead.cause Incorrect usage of `PortalWithState` where its function child returns the render props object directly instead of calling the `portal` function with the content to be portaled.fixThe function child of `PortalWithState` must return React elements, and the actual content to be portaled should be passed as an argument to the `portal` render prop function. For example: `{({ openPortal, portal }) => (<button onClick={openPortal}>{portal(<p>Content</p>)}</button>)}`.
Warnings
- breaking Version 4.0.0 was a complete rewrite, dropping support for React versions older than 16.0.0, as it fully adopted the official `ReactDOM.createPortal` API. While v4.1.0 added a fallback for React 15, initial upgrade to 4.0.0 required React 16+.
- breaking Since version 3.0.0, direct styling (inline styles or `className`) on the main `react-portal` component's root div is no longer supported. The component is intended to be unstyled to avoid DOM clutter.
- breaking In version 2.0.0, the element designated by the `openByClickOn` prop is no longer wrapped by an unnecessary `div` element, and the `openByClickOn` className was removed. This was done to provide cleaner markup, particularly for inline elements like buttons.
- gotcha When using `PortalWithState`, ensure the single child is a function that returns actual React elements. The content to be portaled must be explicitly passed to the `portal` render prop function.
Install
-
npm install react-portal -
yarn add react-portal -
pnpm add react-portal
Imports
- Portal
const { Portal } = require('react-portal');import { Portal } from 'react-portal'; - PortalWithState
import PortalWithState from 'react-portal/lib/PortalWithState';
import { PortalWithState } from 'react-portal'; - Types
import type { PortalProps, PortalWithStateProps } from 'react-portal';
Quickstart
import React, { useState, useEffect } from 'react';
import { Portal, PortalWithState } from 'react-portal';
import { createRoot } from 'react-dom/client';
const App = () => {
const [isBasicPortalOpen, setIsBasicPortalOpen] = useState(false);
// For demonstration purposes, create a target node if it doesn't exist
useEffect(() => {
if (typeof document !== 'undefined' && !document.getElementById('my-custom-portal-target')) {
const div = document.createElement('div');
div.id = 'my-custom-portal-target';
document.body.appendChild(div);
}
}, []);
return (
<div>
<h1>React-Portal Example</h1>
<h2>Basic Portal</h2>
<button onClick={() => setIsBasicPortalOpen(!isBasicPortalOpen)}>
Toggle Basic Portal ({isBasicPortalOpen ? 'Open' : 'Closed'})
</button>
{isBasicPortalOpen && (
<Portal>
<div style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: 'white',
border: '2px solid blue',
padding: '20px',
zIndex: 1000,
boxShadow: '0 4px 8px rgba(0,0,0,0.1)'
}}>
<p>This is a basic portal injected into <code>document.body</code>.</p>
<button onClick={() => setIsBasicPortalOpen(false)}>Close</button>
</div>
</Portal>
)}
<h2>PortalWithState</h2>
<PortalWithState closeOnOutsideClick closeOnEsc>
{({ openPortal, closePortal, isOpen, portal }) => (
<React.Fragment>
<button onClick={openPortal} disabled={isOpen}>
{isOpen ? 'Portal Open' : 'Open Advanced Portal'}
</button>
{portal(
<div style={{
position: 'fixed',
top: '60%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: 'lightgreen',
border: '2px solid darkgreen',
padding: '25px',
zIndex: 1001,
boxShadow: '0 6px 12px rgba(0,0,0,0.15)'
}}>
<p>
This is a more advanced Portal. It handles its own state.
<br />
<button onClick={closePortal}>Close me!</button>, hit ESC or
click outside of me.
</p>
<p>Status: {isOpen ? 'Open' : 'Closed'}</p>
</div>
)}
</React.Fragment>
)}
</PortalWithState>
<h2>Custom Node Portal</h2>
<p>This portal targets a custom div with id 'my-custom-portal-target'.</p>
<Portal node={document && document.getElementById('my-custom-portal-target')}>
<div style={{ border: '1px dashed orange', padding: '10px', marginTop: '10px' }}>
Content portaled to #my-custom-portal-target.
</div>
</Portal>
</div>
);
};
// Mount the App to a root element
if (typeof document !== 'undefined') {
const rootElement = document.getElementById('root');
if (!rootElement) {
const newRoot = document.createElement('div');
newRoot.id = 'root';
document.body.appendChild(newRoot);
}
const root = createRoot(document.getElementById('root')!); // Non-null assertion is safe after check
root.render(<App />);
}