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.