繁体   English   中英

如何让 React Portal 与 React Hook 一起工作?

[英]How can I make React Portal work with React Hook?

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

我在 CodeSandbox 中有一个示例,但我也会在这里发布以防网站关闭或其他原因:

https://codesandbox.io/s/k20poxz2j7

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

 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>

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

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

编辑

按钮 onClick 事件,调用功能组件PopupWindowWithHooks 的第一次调用,它按预期工作(创建新的<div> ,在 useEffect 中将<div>附加到弹出窗口)。

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

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

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

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

编辑2

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

您可以创建一个小的辅助钩子,它会首先在 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>
    );
}

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

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

const [containerEl] = useState(() => document.createElement('div'));
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);
};

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

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

问题是:在每次渲染时都会创建一个新的div ,只需在渲染函数之外创建div ,它就会按预期工作,

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

https://codesandbox.io/s/q9k8q903z6

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

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

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

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

React Hooks 和 Next.js Portal 组件

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

你也可以只使用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>
      )}
    </>
  )
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM