【问题标题】:How can I make React Portal work with React Hook?如何使 React Portal 与 React Hook 一起工作?
【发布时间】:2025-12-23 19:15:06
【问题描述】:

我特别需要在浏览器中侦听自定义事件,然后我有一个按钮可以打开一个弹出窗口。我目前正在使用 React Portal 打开另一个窗口(PopupWindow),但是当我在其中使用钩子时它不起作用 - 但如果我使用类则可以。通过工作,我的意思是,当窗口打开时,两者都显示它下面的 div,但是当事件中的数据刷新时,带有钩子的那个会删除它。要进行测试,请将窗口打开至少 5 秒钟。

我在 CodeSandbox 中有一个示例,但我也会在这里发布,以防网站关闭或出现问题:

https://codesandbox.io/s/k20poxz2j7

下面的代码不会运行,因为我不知道如何通过 react cdn 使 react 钩子工作,但您现在可以使用上面的链接对其进行测试

const { useState, useEffect } = React;
function getRandom(min, max) {
  const first = Math.ceil(min)
  const last = Math.floor(max)
  return Math.floor(Math.random() * (last - first + 1)) + first
}
function replaceWithRandom(someData) {
  let newData = {}
  for (let d in someData) {
    newData[d] = getRandom(someData[d], someData[d] + 500)
  }
  return newData
}

const PopupWindowWithHooks = props => {
  const containerEl = document.createElement('div')
  let externalWindow = null

  useEffect(
    () => {
      externalWindow = window.open(
        '',
        '',
        `width=600,height=400,left=200,top=200`
      )

      externalWindow.document.body.appendChild(containerEl)
      externalWindow.addEventListener('beforeunload', () => {
        props.closePopupWindowWithHooks()
      })
      console.log('Created Popup Window')
      return function cleanup() {
        console.log('Cleaned up Popup Window')
        externalWindow.close()
        externalWindow = null
      }
    },
    // Only re-renders this component if the variable changes
    []
  )
  return ReactDOM.createPortal(props.children, containerEl)
}

class PopupWindow extends React.Component {
  containerEl = document.createElement('div')
  externalWindow = null
  componentDidMount() {
    this.externalWindow = window.open(
      '',
      '',
      `width=600,height=400,left=200,top=200`
    )
    this.externalWindow.document.body.appendChild(this.containerEl)
    this.externalWindow.addEventListener('beforeunload', () => {
      this.props.closePopupWindow()
    })
    console.log('Created Popup Window')
  }
  componentWillUnmount() {
    console.log('Cleaned up Popup Window')
    this.externalWindow.close()
  }
  render() {
    return ReactDOM.createPortal(
      this.props.children,
      this.containerEl
    )
  }
}

function App() {
  let data = {
    something: 600,
    other: 200
  }
  let [dataState, setDataState] = useState(data)
  useEffect(() => {
    let interval = setInterval(() => {
      setDataState(replaceWithRandom(dataState))
      const event = new CustomEvent('onOverlayDataUpdate', {
        detail: dataState
      })
      document.dispatchEvent(event)
    }, 5000)
    return function clear() {
      clearInterval(interval)
    }
  }, [])
  useEffect(
    function getData() {
      document.addEventListener('onOverlayDataUpdate', e => {
        setDataState(e.detail)
      })
      return function cleanup() {
        document.removeEventListener(
          'onOverlayDataUpdate',
          document
        )
      }
    },
    [dataState]
  )
  console.log(dataState)

  // State handling
  const [isPopupWindowOpen, setIsPopupWindowOpen] = useState(false)
  const [
    isPopupWindowWithHooksOpen,
    setIsPopupWindowWithHooksOpen
  ] = useState(false)
  const togglePopupWindow = () =>
    setIsPopupWindowOpen(!isPopupWindowOpen)
  const togglePopupWindowWithHooks = () =>
    setIsPopupWindowWithHooksOpen(!isPopupWindowWithHooksOpen)
  const closePopupWindow = () => setIsPopupWindowOpen(false)
  const closePopupWindowWithHooks = () =>
    setIsPopupWindowWithHooksOpen(false)

  // Side Effect
  useEffect(() =>
    window.addEventListener('beforeunload', () => {
      closePopupWindow()
      closePopupWindowWithHooks()
    })
  )
  return (
    <div>
      <button type="buton" onClick={togglePopupWindow}>
        Toggle Window
      </button>
      <button type="buton" onClick={togglePopupWindowWithHooks}>
        Toggle Window With Hooks
      </button>
      {isPopupWindowOpen && (
        <PopupWindow closePopupWindow={closePopupWindow}>
          <div>What is going on here?</div>
          <div>I should be here always!</div>
        </PopupWindow>
      )}
      {isPopupWindowWithHooksOpen && (
        <PopupWindowWithHooks
          closePopupWindowWithHooks={closePopupWindowWithHooks}
        >
          <div>What is going on here?</div>
          <div>I should be here always!</div>
        </PopupWindowWithHooks>
      )}
    </div>
  )
}

const rootElement = document.getElementById('root')
ReactDOM.render(<App />, rootElement)
<script crossorigin src="https://unpkg.com/react@16.7.0-alpha.2/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16.7.0-alpha.2/umd/react-dom.development.js"></script>
<div id="root"></div>

【问题讨论】:

    标签: javascript reactjs react-hooks


    【解决方案1】:

    const [containerEl] = useState(document.createElement('div'));

    编辑

    按钮onClick事件,调用PopupWindowWithHooks功能组件的first调用,它按预期工作(创建新的&lt;div&gt;,在useEffect中追加&lt;div&gt;到弹出窗口) .

    事件刷新,调用功能组件PopupWindowWithHooks第二次调用,再次const containerEl = document.createElement('div')行创建新的&lt;div&gt;。但是(第二个)新的&lt;div&gt; 永远不会附加到弹出窗口,因为行externalWindow.document.body.appendChild(containerEl) 是在useEffect 挂钩中,它只在挂载时运行并在卸载时清理(第二个参数是一个空数组[])。

    最后return ReactDOM.createPortal(props.children, containerEl) 使用第二个参数创建门户containerEl - 新的未附加&lt;div&gt;

    containerEl作为有状态值(useState hook),问题就解决了:

    const [containerEl] = useState(document.createElement('div'));
    

    EDIT2

    代码沙盒:https://codesandbox.io/s/l5j2zp89k9

    【讨论】:

    • 虽然此代码 sn-p 可能是解决方案,但包含解释确实有助于提高帖子的质量。请记住,您是在为将来的读者回答问题,而这些人可能不知道您提出代码建议的原因。
    • 关于代码的事情是,如果您让弹出窗口保持打开状态,则内容永远不会被 PopupWindow 删除。使用 PopupWindowWithHooks 它会被删除。我的问题是关于尝试使门户与钩子一起工作。即使将 div 置于 useState 它仍然不起作用。也许把它放在 useEffect 里面会起作用。我将尝试这样做,因为您对为什么永远不会重新创建 div 的解释是有道理的。编辑:不,即使在 useEffect 内,内容仍然会被删除。也许在 containerEl 上使用 useRef 使其持久化? EDIT2:也不适用于 useRef()。
    • 谢谢,解决了。我不知道为什么它以前不起作用,但我怀疑我的 setInterval 函数不仅仅是帮助。
    • 你应该使用const element = useRef(document.createElement('div'))
    【解决方案2】:

    问题是:每次渲染都会创建一个新的div,只需在外部渲染创建div 功能,它应该按预期工作,

    const containerEl = document.createElement('div')
    const PopupWindowWithHooks = props => {
       let externalWindow = null
       ... rest of your code ...
    

    https://codesandbox.io/s/q9k8q903z6

    【讨论】:

    • 这里只是好奇,因为它们只是函数,你如何命名它们渲染函数?
    【解决方案3】:
    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);
    };
    

    然后将门户与您的模式/弹出窗口一起使用:

    const App = () => (
      <Portal>
        <MyModal />
      </Portal>
    )
    

    【讨论】:

      【解决方案4】:

      你可以创建一个小的辅助钩子,它会首先在 dom 中创建一个元素:

      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);
      }
      

      您的组件可能如下所示:

      const Demo = () => {
          const createBodyPortal = useCreatePortalInBody();
          return createBodyPortal(
              <div style={{position: 'fixed', top: 0, left: 0}}>
                  In body
              </div>
          );
      }
      

      请注意,此解决方案在服务器端渲染期间不会渲染任何内容。

      【讨论】:

        【解决方案5】:

        您也可以只使用react-useportal。它的工作原理是:

        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>
              )}
            </>
          )
        }
        

        【讨论】:

        • 可能应该提到你是作者
        【解决方案6】:

        Thought id 采用了一个对我来说效果很好的解决方案,它动态地创建了一个门户元素,通过 props 具有可选的 className 和元素类型,并在组件卸载时删除所述元素:

        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)
        }
        
        

        【讨论】:

          【解决方案7】:

          选择/流行的答案很接近,但它不必要地在每次渲染上创建未使用的 DOM 元素。 useState 钩子可以提供一个函数来确保初始值只被创建一次:

          const [containerEl] = useState(() => document.createElement('div'));
          

          【讨论】:

          • 你是对的!可以使用以下代码验证此行为:const whatever = useState(console.log('useState')); const [count, setCount] = useState(0); setTimeout(() =&gt; setCount(count + 1), 1000); console.log('render', whatever);
          【解决方案8】:

          如果您正在使用 Next.js,您会注意到许多解决方案都不起作用,因为元素选择器使用了 documentwindow 对象。由于服务器端渲染限制,这些仅在 useEffect 挂钩等中可用。

          我为自己创建了这个解决方案来处理 Next.js 和 ReactDOM.createPortal 功能而不会破坏任何东西。

          如果其他人愿意,可以修复一些已知问题:

          1. 我不喜欢必须创建一个元素并将其附加到documentElement(可以或应该是document?)并且还为模态内容创建一个空容器。我觉得这可以缩小很多。我试过了,但由于 SSR 和 Next.js 的性质,它变成了意大利面条代码。
          2. 内容(即使您使用多个&lt;Portal&gt; 元素)始终会添加到您的页面中,但不会在服务器端呈现期间。这意味着谷歌和其他搜索引擎仍然可以索引您的内容,只要他们等待 JavaScript 完成其在客户端的工作。如果有人可以修复此问题以同时呈现服务器端,这样初始页面加载即可为访问者提供完整内容,那就太好了。

          React Hooks 和 Next.js 门户组件

          /**
           * Create a React Portal to contain the child elements outside of your current
           * component's context.
           * @param visible {boolean} - Whether the Portal is visible or not. This merely changes the container's styling.
           * @param containerId {string} - The ID attribute used for the Portal container. Change to support multiple Portals.
           * @param children {JSX.Element} - A child or list of children to render in the document.
           * @return {React.ReactPortal|null}
           * @constructor
           */
          const Portal = ({ visible = false, containerId = 'modal-root', children }) => {
            const [modalContainer, setModalContainer] = useState();
          
            /**
             * Create the modal container element that we'll put the children in.
             * Also make sure the documentElement has the modal root element inserted
             * so that we do not have to manually insert it into our HTML.
             */
            useEffect(() => {
              const modalRoot = document.getElementById(containerId);
              setModalContainer(document.createElement('div'));
          
              if (!modalRoot) {
                const containerDiv = document.createElement('div');
                containerDiv.id = containerId;
                document.documentElement.appendChild(containerDiv);
              }
            }, [containerId]);
          
            /**
             * If both the modal root and container elements are present we want to
             * insert the container into the root.
             */
            useEffect(() => {
              const modalRoot = document.getElementById(containerId);
          
              if (modalRoot && modalContainer) {
                modalRoot.appendChild(modalContainer);
              }
          
              /**
               * On cleanup we remove the container from the root element.
               */
              return function cleanup() {
                if (modalContainer) {
                  modalRoot.removeChild(modalContainer);
                }
              };
            }, [containerId, modalContainer]);
          
            /**
             * To prevent the non-visible elements from taking up space on the bottom of
             * the documentElement, we want to use CSS to hide them until we need them.
             */
            useEffect(() => {
              if (modalContainer) {
                modalContainer.style.position = visible ? 'unset' : 'absolute';
                modalContainer.style.height = visible ? 'auto' : '0px';
                modalContainer.style.overflow = visible ? 'auto' : 'hidden';
              }
            }, [modalContainer, visible]);
          
            /**
             * Make sure the modal container is there before we insert any of the
             * Portal contents into the document.
             */
            if (!modalContainer) {
              return null;
            }
          
            /**
             * Append the children of the Portal component to the modal container.
             * The modal container already exists in the modal root.
             */
            return ReactDOM.createPortal(children, modalContainer);
          };
          

          如何使用:

          const YourPage = () => {
            const [isVisible, setIsVisible] = useState(false);
            return (
              <section>
                <h1>My page</h1>
          
                <button onClick={() => setIsVisible(!isVisible)}>Toggle!</button>
          
                <Portal visible={isVisible}>
                  <h2>Your content</h2>
                  <p>Comes here</p>
                </Portal>
              </section>
            );
          }
          

          【讨论】: