Как я могу заставить React Portal работать с React Hook? - PullRequest
0 голосов
/ 03 декабря 2018

У меня есть особая потребность слушать пользовательское событие в браузере, и оттуда у меня есть кнопка, которая открывает всплывающее окно.В настоящее время я использую React Portal, чтобы открыть это другое окно (PopupWindow), но когда я использую хуки внутри, оно не работает - но работает, если я использую классы.Под работой я подразумеваю, что когда открывается окно, оба показывают div под ним, но тот, у которого есть хуки, стирает его при обновлении данных из события.Чтобы проверить, оставьте окно открытым как минимум на 5 секунд.

У меня есть пример в CodeSandbox, но я также публикую здесь сообщение на случай, если сайт не работает или что-то еще:

https://codesandbox.io/s/k20poxz2j7

Приведенный ниже код не будет работать, потому что я не знаю, как заставить реагирующие хуки работать через реагирующий CDN, но вы можете проверить это по ссылке выше

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>

Ответы [ 5 ]

0 голосов
/ 18 апреля 2019

Вы также можете просто использовать 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>
      )}
    </>
  )
}
0 голосов
/ 15 марта 2019

Вы можете создать небольшой вспомогательный хук, который сначала создаст элемент в 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>
    );
}

Обратите внимание, что этоРешение не будет ничего делать во время рендеринга на стороне сервера.

0 голосов
/ 10 декабря 2018

Проблема в том, что при каждом рендеринге создается новый div, просто создайте div внешнюю функцию рендеринга, и она должна работать как положено,

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

https://codesandbox.io/s/q9k8q903z6

0 голосов
/ 13 марта 2019
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>
)
0 голосов
/ 05 декабря 2018

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

РЕДАКТИРОВАТЬ

Кнопка событие onClick, вызов первый вызов функционального компонента PopupWindowWithHooks и егоработает как ожидалось (создайте новый <div>, в useEffect добавьте <div> к всплывающему окну).

Обновление события, вызов второй вызов функционального компонента PopupWindowWithHooks и строку const containerEl = document.createElement('div') снова создайте <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

...