我特别需要在浏览器中侦听自定义事件,并且从那里,我有一个可以打开弹出窗口的按钮。我目前正在使用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>
答案 0 :(得分:4)
选择/受欢迎的答案很接近,但是不必要在每个渲染上创建未使用的DOM元素。可以为useState
钩子提供一个函数,以确保仅创建一次初始值:
const [containerEl] = useState(() => document.createElement('div'));
答案 1 :(得分:3)
const [containerEl] = useState(document.createElement('div'));
编辑
按钮onClick事件,调用功能组件 PopupWindowWithHooks 的第一个调用,它可以按预期工作(在useEffect附加<div>
中创建新的<div>
弹出窗口。
事件刷新,调用功能组件 PopupWindowWithHooks 的 second 调用,并在行const containerEl = document.createElement('div')
中再次创建新的<div>
。但是,(第二个)新的<div>
将永远不会追加到弹出窗口,因为行externalWindow.document.body.appendChild(containerEl)
在useEffect钩子中,该钩子只能在mount上运行,而在unmount上清除(第二个参数是一个空数组[ ])。
最后return ReactDOM.createPortal(props.children, containerEl)
创建带有第二个参数 containerEl 的门户-新的未附加的<div>
使用 containerEl 作为有状态值(useState挂钩),问题得以解决:
const [containerEl] = useState(document.createElement('div'));
EDIT2
答案 2 :(得分:3)
您可以创建一个小的辅助钩,该钩将首先在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>
);
}
请注意,此解决方案在服务器端渲染期间不会渲染任何内容。
答案 3 :(得分:0)
问题是:在每个渲染上都会创建一个新的div
,只需在外部渲染上创建div
功能,并且应该可以正常工作
const containerEl = document.createElement('div')
const PopupWindowWithHooks = props => {
let externalWindow = null
... rest of your code ...
答案 4 :(得分:0)
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>
)
答案 5 :(得分:0)
您也可以只使用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 :(得分:0)
认为id与适用于我的解决方案非常有效,该解决方案可以动态创建门户元素,并通过props使用可选的className和element type来创建门户元素,并在组件卸载时删除该元素:
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 :(得分:0)
如果您使用 Next.js,您会注意到许多解决方案由于使用 document
或 window
对象的元素选择器而不起作用。由于服务器端呈现限制,这些仅在 useEffect
钩子等中可用。
我为自己创建了这个解决方案来处理 Next.js 和 ReactDOM.createPortal
功能,而不会破坏任何东西。
其他人可以根据需要修复一些已知问题:
documentElement
(可以或应该是 document
?),也不想为模态内容创建一个空容器。我觉得这可以缩小很多。我尝试过,但由于 SSR 和 Next.js 的性质,它变成了意大利面条式代码。<Portal>
元素)始终会添加到您的页面,但不会在服务器端呈现期间。这意味着 Google 和其他搜索引擎仍然可以索引您的内容,只要它们等待 JavaScript 在客户端完成其工作即可。如果有人可以修复此问题以同时呈现服务器端,以便初始页面加载为访问者提供完整内容,那就太好了。/**
* 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>
);
}