Search code examples
reactjsreact-dom

React createPortal call fails because document.body doesn't exist?


I've got a React component for a modal dialog. It works just fine when I don't do anything special with it:

return (
    <Modal className={isShowing ? "modal is-active" : "modal"}>
        <ModalBackground></ModalBackground>
        <ModalCard>
            <ModalHeader>
                <ModalTitle>
                    <NoticeIcon type={noticeType}/>
                    {title}
                </ModalTitle>
                <ModalClose onClick={handleHideModal}></ModalClose>
            </ModalHeader>
            <ModalContent>
                {content}
            </ModalContent>
            <ModalFoot>
                { buttonSpecs.map((spec) => 
                    <BottomButton key={spec.label} label={spec.label} handleClick={spec.handleClick} isPrimary={spec.isPrimary} />
                )}
            </ModalFoot>
        </ModalCard>
    </Modal>
);

However, the prescribed approach for displaying a dialog is to display it at the bottom of the DOM document, rather than wedged inside whatever component happens to be invoking it, including one into which the component's outer DOM element may not even be allowed. (Indeed, in one case, I'm invoking a modal dialog from inside a component that renders a table row, at TR. It "works" but it triggers an error message to the Chrome console, as nothing but TH and TD are allowed inside a TR). The way to do that is with ReactDOM.createPortal:

import ReactDOM from 'react-dom';
...
return (ReactDOM.createPortal(
    <Modal className={isShowing ? "modal is-active" : "modal"}>
        <ModalBackground></ModalBackground>
        <ModalCard>
            <ModalHeader>
                <ModalTitle>
                    <NoticeIcon type={noticeType}/>
                    {title}
                </ModalTitle>
                <ModalClose onClick={handleHideModal}></ModalClose>
            </ModalHeader>
            <ModalContent>
                {content}
            </ModalContent>
            <ModalFoot>
                { buttonSpecs.map((spec) => 
                    <BottomButton key={spec.label} label={spec.label} handleClick={spec.handleClick} isPrimary={spec.isPrimary} />
                )}
            </ModalFoot>
        </ModalCard>
    </Modal>
), document.body);

The call to ReactDOM.createPortal, with document.body as its second parameter, is supposed to render the component at the end of the body. But it's giving me an error: "Target container is not a DOM element".

At the time I'm displaying the dialog, the page and everything else in it already exists, so the body exists.

I've tried another approach I've seen explained, which is to create an explicit div at the end of the outer HTML structure of the document as a container for the modal:

<div id="root"></div>
<div id="modalRoot"></div>

and then substitute document.getElementById('modalRoot') for document.body. But that's giving me the same error.

The only answers I've found for this involve cases where the modal is being invoked on first rendering, by script that's in the HEAD section of the document, so that the body doesn't already exist. The advice in those cases has been to move the script to the end of the document. In my case, the dialog is being displayed in response to some action of mine inside a document that's already been rendered.

Any ideas?


Solution

  • import ReactDOM from 'react-dom';
    ...
    return ReactDOM.createPortal(
        <Modal className={isShowing ? "modal is-active" : "modal"}>
            <ModalBackground></ModalBackground>
            <ModalCard>
                <ModalHeader>
                    <ModalTitle>
                        <NoticeIcon type={noticeType}/>
                        {title}
                    </ModalTitle>
                    <ModalClose onClick={handleHideModal}></ModalClose>
                </ModalHeader>
                <ModalContent>
                    {content}
                </ModalContent>
                <ModalFoot>
                    { buttonSpecs.map((spec) => 
                        <BottomButton key={spec.label} label={spec.label} handleClick={spec.handleClick} isPrimary={spec.isPrimary} />
                    )}
                </ModalFoot>
            </ModalCard>
        </Modal>
    ), document.body);
    

    Your function should return object returned by ReactDOM.createPortal function. You added extra ( at start of return function. Refer : https://reactjs.org/docs/portals.html#usage