web-dev-qa-db-fra.com

Comment puis-je faire fonctionner React Portal avec React Hook?)

J'ai ce besoin spécifique d'écouter un événement personnalisé dans le navigateur et à partir de là, j'ai un bouton qui ouvrira une fenêtre contextuelle. J'utilise actuellement React Portal pour ouvrir cette autre fenêtre (PopupWindow), mais lorsque j'utilise des crochets à l'intérieur, cela ne fonctionne pas - mais fonctionne si j'utilise des classes. En travaillant, je veux dire, quand la fenêtre s'ouvre, les deux affichent le div en dessous mais celui avec des crochets l'efface lorsque les données de l'événement sont actualisées. Pour tester, laissez la fenêtre ouverte pendant au moins 5 secondes.

J'ai un exemple dans un CodeSandbox, mais je poste également ici au cas où le site Web est en panne ou quelque chose:

https://codesandbox.io/s/k20poxz2j7

Le code ci-dessous ne fonctionnera pas car je ne sais pas comment faire fonctionner les hooks via react cdn mais vous pouvez le tester avec le lien ci-dessus maintenant

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/[email protected]/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<div id="root"></div>
5
bsides

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

[~ # ~] modifier [~ # ~]

Bouton onClick, invoquez premier appel du composant fonctionnel PopupWindowWithHooks et cela fonctionne comme prévu (créez un nouveau <div>, dans useEffect, ajoutez <div> à la fenêtre contextuelle).

L'événement s'actualise, appelle le deuxième appel du composant fonctionnel PopupWindowWithHooks et la ligne const containerEl = document.createElement('div') create new <div> À nouveau. Mais ce (deuxième) nouveau <div> Ne sera jamais ajouté à la fenêtre contextuelle, car la ligne externalWindow.document.body.appendChild(containerEl) est en cours d'utilisation. Le crochet ne fonctionnerait que lors du montage et du nettoyage lors du démontage (le deuxième argument est un tableau vide []).

Enfin return ReactDOM.createPortal(props.children, containerEl) créer un portail avec un deuxième argument containerEl - nouveau non ajouté <div>

Avec containerEl comme valeur avec état (hook useState), le problème est résolu:

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

EDIT2

Code Sandbox: https://codesandbox.io/s/l5j2zp89k9

2
Boris Traljić
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);
};

Utilisez ensuite le portail avec votre modal/popup:

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

J'ai pensé que le carillon id avec une solution qui a très bien fonctionné pour moi qui crée un élément de portail dynamiquement, avec facultatif className et type d'élément via des accessoires et supprime ledit élément lorsque le composant se démonte:

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

1
Samuel

Le problème est: un nouveau div est créé sur chaque rendu, il suffit de créer la fonction de rendu extérieur div et cela devrait fonctionner comme prévu,

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

https://codesandbox.io/s/q9k8q903z6

0
Walid Ammar