[英]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 ...
如果您正在使用 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>
);
}
你也可以只使用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.