How can I make React Portal work with React Hook?
const [containerEl] = useState(document.createElement('div'));
EDIT
Button onClick event, invoke first call of functional component PopupWindowWithHooks and it works as expected (create new <div>
, in useEffect append <div>
to popup window).
The event refresh, invoke second call of functional component PopupWindowWithHooks and line const containerEl = document.createElement('div')
create new <div>
again. But that (second) new <div>
will never be appended to popup window, because line externalWindow.document.body.appendChild(containerEl)
is in useEffect hook that would run only on mount and clean up on unmount (the second argument is an empty array []).
Finally return ReactDOM.createPortal(props.children, containerEl)
create portal with second argument containerEl - new unappended <div>
With containerEl as a stateful value (useState hook), problem is solved:
const [containerEl] = useState(document.createElement('div'));
EDIT2
Code Sandbox: https://codesandbox.io/s/l5j2zp89k9
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)
}
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.