How can I make React Portal work with React Hook?

后端 未结 7 2013
感动是毒
感动是毒 2021-01-01 22:05

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

相关标签:
7条回答
  • 2021-01-01 22:11
    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>
    )
    
    0 讨论(0)
  • 2021-01-01 22:14

    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.

    0 讨论(0)
  • 2021-01-01 22:17

    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)
    }
    
    
    0 讨论(0)
  • 2021-01-01 22:20

    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

    0 讨论(0)
  • 2021-01-01 22:26

    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>
          )}
        </>
      )
    }
    
    0 讨论(0)
  • 2021-01-01 22:27

    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'));
    
    0 讨论(0)
提交回复
热议问题