I have this specific need to listen to a custom event in the browser and from there, I have a button that will open a popup window. I\'m currently using React Portal to open
const Portal = ({ children }) => {
const [modalContainer] = useState(document.createElement('div'));
useEffect(() => {
// Find the root element in your DOM
let modalRoot = document.getElementById('modal-root');
// If there is no root then create one
if (!modalRoot) {
const tempEl = document.createElement('div');
tempEl.id = 'modal-root';
document.body.append(tempEl);
modalRoot = tempEl;
}
// Append modal container to root
modalRoot.appendChild(modalContainer);
return function cleanup() {
// On cleanup remove the modal container
modalRoot.removeChild(modalContainer);
};
}, []); // <- The empty array tells react to apply the effect on mount/unmount
return ReactDOM.createPortal(children, modalContainer);
};
Then use the Portal with your modal/popup:
const App = () => (
<Portal>
<MyModal />
</Portal>
)
You could create a small helper hook which would create an element in the dom first:
import { useLayoutEffect, useRef } from "react";
import { createPortal } from "react-dom";
const useCreatePortalInBody = () => {
const wrapperRef = useRef(null);
if (wrapperRef.current === null && typeof document !== 'undefined') {
const div = document.createElement('div');
div.setAttribute('data-body-portal', '');
wrapperRef.current = div;
}
useLayoutEffect(() => {
const wrapper = wrapperRef.current;
if (!wrapper || typeof document === 'undefined') {
return;
}
document.body.appendChild(wrapper);
return () => {
document.body.removeChild(wrapper);
}
}, [])
return (children => wrapperRef.current && createPortal(children, wrapperRef.current);
}
And your component could look like this:
const Demo = () => {
const createBodyPortal = useCreatePortalInBody();
return createBodyPortal(
<div style={{position: 'fixed', top: 0, left: 0}}>
In body
</div>
);
}
Please note that this solution would not render anything during server side rendering.
Thought id chime in with a solution that has worked very well for me which creates a portal element dynamically, with optional className and element type via props and removes said element when the component unmounts:
export const Portal = ({
children,
className = 'root-portal',
element = 'div',
}) => {
const [container] = React.useState(() => {
const el = document.createElement(element)
el.classList.add(className)
return el
})
React.useEffect(() => {
document.body.appendChild(container)
return () => {
document.body.removeChild(container)
}
}, [])
return ReactDOM.createPortal(children, container)
}
The issue is: a new div
is created on every render, just create the div
outside render
function and it should work as expected,
const containerEl = document.createElement('div')
const PopupWindowWithHooks = props => {
let externalWindow = null
... rest of your code ...
https://codesandbox.io/s/q9k8q903z6
You could also just use react-useportal. It works like:
import usePortal from 'react-useportal'
const App = () => {
const { openPortal, closePortal, isOpen, Portal } = usePortal()
return (
<>
<button onClick={openPortal}>
Open Portal
</button>
{isOpen && (
<Portal>
<p>
This is more advanced Portal. It handles its own state.{' '}
<button onClick={closePortal}>Close me!</button>, hit ESC or
click outside of me.
</p>
</Portal>
)}
</>
)
}
The chosen/popular answer is close, but it needlessly creates unused DOM elements on every render. The useState
hook can be supplied a function to make sure the initial value is only created once:
const [containerEl] = useState(() => document.createElement('div'));