模板对于快速引起用户的注意非常有用。它们可以用来收集用户信息,提供更新,或鼓励用户采取行动。一项对20亿个弹出式广告的研究显示,表现最好的10%有超过9%的转换率。
然而,我认为可以这样说,模态的建立需要一些耐心。要跟踪所有的Z-index值、层和DOM层次结构并不容易。这种困难也延伸到了其他需要在顶层渲染的元素,如覆盖或工具提示。
在React应用程序中,一个组件或元素作为最近的父节点的一个子节点被装入DOM中。从上到下,标准的层级结构如下。root node => parent nodes => child nodes => leaf nodes 。
如果父节点有溢出隐藏属性或者在更高的层有元素,那么子节点就不能出现在顶层,而被限制在父节点的可见区域。我们可以尝试设置一个非常高的z-index值来把子节点带到顶层,但这个策略可能很繁琐,而且不一定成功。
这就是React Portals出现的地方。React Portals为一个元素提供了在默认层次结构之外渲染的能力,而不影响组件之间的父子关系。
在这篇文章中,我们将演示如何使用React Portals在React中建立一个模态。本文所使用的方法也可以应用于构建工具提示、全页面顶层侧边栏、全局搜索overalls或隐藏的溢出式父容器内的下拉框。
那么,废话不多说,让我们开始这个魔术吧......
开始吧
让我们先用Create React App模板或你自己的React应用设置来创建一个新的React应用。
# using yarn
yarn create react-app react-portal-overlay
# using npx
npx create-react-app react-portal-overlay
接下来,切换到app目录并启动React应用。
# cd into app directory
cd react-portal-overlay
# start using yarn
yarn start
# start using npm
npm run start
组件概述
我们将创建两个组件,并在模板中已经可用的app组件中渲染它们。
但首先,这里有一些重要的定义。
ReactPortal: 一个包装组件,在默认的层次结构之外,在提供的容器中创建一个Portal并渲染内容Modal: 一个基本的模态组件,它的JSX内容要用ReactPortalApp(任何组件):我们将使用Modal组件并保持其活动状态(打开或关闭)的位置
创建React Portal
可以使用react-dom 中的createPortal来创建一个React Portal。它需要两个参数。
content:任何有效的可渲染React元素containerElement:一个有效的DOM元素,我们可以将其附加到该元素上。content
ReactDOM.createPortal(content, containerElement);
我们将在src/components 目录下创建一个新的组件,ReactPortal.js ,并添加这个片段。
// src/components/ReactPortal.js
import { createPortal } from 'react-dom';
function ReactPortal({ children, wrapperId }) {
return createPortal(children, document.getElementById(wrapperId));
}
export default ReactPortal;
ReactPortal 组件接受wrapperId 属性,它是一个DOM元素的ID。我们用这段代码找到一个具有所提供的ID的元素,并将其作为门户网站的containerElement 。
值得注意的是,createPortal() 函数不会为我们创建containerElement 。该函数希望containerElement 已经在DOM中可用。这就是为什么我们必须自己添加它,以使门户在该元素中呈现内容。
我们可以定制ReactPortal 组件,如果在DOM中找不到这样的元素,就用提供的ID创建一个元素。
首先,我们添加一个辅助函数,用给定的ID创建一个空的div ,将其追加到正文中,并返回该元素。
function createWrapperAndAppendToBody(wrapperId) {
const wrapperElement = document.createElement('div');
wrapperElement.setAttribute("id", wrapperId);
document.body.appendChild(wrapperElement);
return wrapperElement;
}
接下来,让我们更新ReactPortal 组件以使用createWrapperAndAppendToBody 辅助方法。
// Also, set a default value for wrapperId prop if none provided
function ReactPortal({ children, wrapperId = "react-portal-wrapper" }) {
let element = document.getElementById(wrapperId);
// if element is not found with wrapperId,
// create and append to body
if (!element) {
element = createWrapperAndAppendToBody(wrapperId);
}
return createPortal(children, element);
}
这个方法有一个限制。如果wrapperId 属性发生变化,ReactPortal 组件将无法处理最新的属性值。为了解决这个问题,我们需要将任何依赖于wrapperId 的逻辑转移到另一个操作或副作用。
处理一个动态wrapperId
React HooksuseLayoutEffect 和useEffect 实现了类似的结果,但用法略有不同。一个快速的经验法则是,如果效果需要同步,而且在DOM上有任何直接的突变,那么就使用useLayoutEffect 。由于这种情况相当罕见,useEffect 通常是最好的选择。useEffect 异步运行。
在这种情况下,我们直接突变了DOM,并希望在DOM被重新绘制之前同步运行效果,所以使用useLayoutEffect Hook更有意义。
首先,让我们把查找元素和创建逻辑移到useLayoutEffect Hook中,把wrapperId 作为依赖关系。接下来,我们将把element 设为状态。当wrapperId 变化时,该组件将相应地更新。
import { useState, useLayoutEffect } from 'react';
// ...
function ReactPortal({ children, wrapperId = "react-portal-wrapper" }) {
const [wrapperElement, setWrapperElement] = useState(null);
useLayoutEffect(() => {
let element = document.getElementById(wrapperId);
// if element is not found with wrapperId or wrapperId is not provided,
// create and append to body
if (!element) {
element = createWrapperAndAppendToBody(wrapperId);
}
setWrapperElement(element);
}, [wrapperId]);
// wrapperElement state will be null on very first render.
if (wrapperElement === null) return null;
return createPortal(children, wrapperElement);
}
现在,我们需要处理清理工作。
处理效果清理
我们直接对DOM进行突变,并在没有找到元素的情况下向主体追加一个空的div 。因此,我们需要确保当ReactPortal 组件被卸载时,动态添加的空div 被从DOM中删除。另外,我们必须避免在清理过程中删除任何现有的元素。
让我们添加一个systemCreated 标志,当createWrapperAndAppendToBody 被调用时,将其设置为true 。如果systemCreated 是true ,我们将从DOM中删除该元素。更新后的useLayoutEffect 将看起来像这样。
// ...
useLayoutEffect(() => {
let element = document.getElementById(wrapperId);
let systemCreated = false;
// if element is not found with wrapperId or wrapperId is not provided,
// create and append to body
if (!element) {
systemCreated = true;
element = createWrapperAndAppendToBody(wrapperId);
}
setWrapperElement(element);
return () => {
// delete the programatically created element
if (systemCreated && element.parentNode) {
element.parentNode.removeChild(element);
}
}
}, [wrapperId]);
// ...
我们已经创建了门户,并将其定制为故障安全的。接下来,让我们创建一个简单的模态组件,并使用React Portal渲染它。
构建一个演示模态
为了构建模态组件,我们首先在src/components 下创建一个新的目录:Modal ,并添加两个新文件:Modal.js 和modalStyles.css 。
这个模态组件接受几个属性。
isOpen:一个布尔标志,代表模态的状态(打开或关闭),由渲染模态的父组件控制。handleClose:一个方法,通过点击关闭按钮或任何触发关闭的动作来调用。
只有当isOpen ,模态组件才会渲染内容true 。false模态组件将在return null ,因为我们不希望在关闭模态时将其保留在DOM中。
// src/components/Modal/Modal.js
import "./modalStyles.css";
function Modal({ children, isOpen, handleClose }) {
if (!isOpen) return null;
return (
<div className="modal">
<button onClick={handleClose} className="close-btn">
Close
</button>
<div className="modal-content">{children}</div>
</div>
);
}
export default Modal;
演示模态的样式
现在,让我们为模态添加一些样式。
/* src/components/Modal/modalStyles.css */
.modal {
position: fixed;
inset: 0; /* inset sets all 4 values (top right bottom left) much like how we set padding, margin etc., */
background-color: rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.3s ease-in-out;
overflow: hidden;
z-index: 999;
padding: 40px 20px 20px;
}
.modal-content {
width: 70%;
height: 70%;
background-color: #282c34;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
}
这段代码将使模态占据整个视口,并在垂直和水平方向上将.modal-content 中心对齐。
用escape键关闭模态
模态可以通过点击Close ,触发handleClose 来关闭。让我们也添加通过按转义键来关闭模态的功能。为了达到这个目的,我们将附加useEffect keydown事件监听器。我们将在效果清理时移除事件监听器。
在一个按键事件中,如果Escape 键被按下,我们将调用handleClose 。
// src/components/Modal/Modal.js
import { useEffect } from "react";
import "./modalStyles.css";
function Modal({ children, isOpen, handleClose }) {
useEffect(() => {
const closeOnEscapeKey = e => e.key === "Escape" ? handleClose() : null;
document.body.addEventListener("keydown", closeOnEscapeKey);
return () => {
document.body.removeEventListener("keydown", closeOnEscapeKey);
};
}, [handleClose]);
if (!isOpen) return null;
return (
<div className="modal">
<button onClick={handleClose} className="close-btn">
Close
</button>
<div className="modal-content">{children}</div>
</div>
);
};
export default Modal;
我们的模态组件现在已经准备就绪,可以开始行动了
摆脱默认的DOM层次结构
让我们在一个应用程序中渲染演示的Modal 组件。
为了控制模态的打开和关闭行为,我们将用useState Hook初始化状态isOpen ,并将其设置为默认的false 。接下来,我们将添加一个按钮点击,button onClick ,将isOpen 状态设置为true ,并打开模态。
现在,我们将把isOpen 和handleClose 作为属性发送给Modal 组件。handleClose 属性只是一个回调方法,它将isOpen 状态设置为false ,以便关闭模态。
// src/App.js
import { useState } from "react";
import logo from "./logo.svg";
import Modal from "./components/Modal/Modal";
import "./App.css";
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<button onClick={() => setIsOpen(true)}>
Click to Open Modal
</button>
<Modal handleClose={() => setIsOpen(false)} isOpen={isOpen}>
This is Modal Content!
</Modal>
</header>
</div>
);
}
export default App;
模态可以通过点击Click to Open Modal按钮来打开。模态可以通过按转义键或点击关闭按钮来关闭。无论哪种操作都会触发handleClose 方法并关闭模态。
如果我们看一下DOM树,我们就会发现,根据默认的DOM层次结构,modal 被渲染为header 的一个子节点。
构建的模态没有ReactPortal 。
让我们用ReactPortal 来包装模态的返回JSX,这样模态就会在DOM层次结构之外和所提供的容器元素中被呈现出来。一个动态的容器被附加在DOM内的主体的最后一个孩子身上。
Modal 组件的最新返回方法应该是这样的。
// src/components/Modal/Modal.js
import ReactPortal from "../ReactPortal";
// ...
function Modal({ children, isOpen, handleClose }) {
// ...
return (
<ReactPortal wrapperId="react-portal-modal-container">
<div className="modal">
// ...
</div>
</ReactPortal>
);
}
// ...
由于我们没有添加一个具有react-portal-modal-container id的容器,一个空的div 将以这个id被创建,然后它将被附加到body上。Modal 组件将在这个新创建的容器中被渲染,在默认的 DOM 层次结构之外。只有生成的HTML和DOM树被改变。
React组件的头和Modal 组件之间的父子关系保持不变。
用ReactPortal 构建的模态。
如下图所示,我们的演示模态渲染正确,但其用户界面的打开和关闭感觉过于瞬时。
不使用CSSTransition 构建的模态。
应用过渡CSSTransition
为了调整模态的打开和关闭的过渡,我们可以在关闭Modal 组件时删除return null 。我们可以通过CSS控制模态的可见性,使用opacity 和transform 属性以及一个有条件添加的类,show/hide 。
这个show/hide 类可以用来设置或重置可见性,并使用过渡属性对打开和关闭进行动画处理。这样做效果很好,只是模态即使在关闭后仍然留在DOM中。
我们也可以将display 属性设置为none ,但这与return null 的结果相同。这两个属性都会立即从DOM中移除元素,而不需要等待过渡或动画完成。这就是[CSSTransition] 组件的用武之地了。
通过将要过渡的元素包裹在[CSSTransition] 组件中,并将unmountOnExit 属性设置为true ,过渡就会运行,一旦过渡完成,该元素就会从 DOM 中删除。
首先,我们安装react-transition-group 依赖关系。
# using yarn
yarn add react-transition-group
# using npm
npm install react-transition-group
接下来,我们导入CSSTransition 组件,用它来包裹模态返回JSX中ReactPortal 下的一切。
该组件的触发、持续时间和样式都可以通过设置CSSTransition 属性来控制。
in: 触发进入或退出状态的布尔标志timeout:每个状态(进入、退出等)的转换时间。unmountOnExit: 退出后解除组件的挂载classNames:每个状态(进入、退出等)的类名都会有后缀,以便于控制CSS的定制nodeRef: 对需要转换的DOM元素的React引用(在本例中,是Modal组件的根div元素)
一个ref ,可以使用useRef Hook来创建。这个值被传递给CSSTransition'snodeRef 属性。它作为一个ref 属性附加到Modal'的根div ,以连接CSSTransition 组件和需要过渡的元素。
// src/components/Modal/Modal.js
import { useEffect, useRef } from "react";
import { CSSTransition } from "react-transition-group";
// ...
function Modal({ children, isOpen, handleClose }) {
const nodeRef = useRef(null);
// ...
// if (!isOpen) return null; <-- Make sure to remove this line.
return (
<ReactPortal wrapperId="react-portal-modal-container">
<CSSTransition
in={isOpen}
timeout={{ entry: 0, exit: 300 }}
unmountOnExit
classNames="modal"
nodeRef={nodeRef}
>
<div className="modal" ref={nodeRef}>
// ...
</div>
</CSSTransition>
<ReactPortal wrapperId="react-portal-modal-container">
);
}
// ....
接下来,让我们为CSSTransition 组件添加的状态前缀类,modal-enter-done 和modal-exit ,添加一些过渡样式。
.modal {
...
opacity: 0;
pointer-events: none;
transform: scale(0.4);
}
.modal-enter-done {
opacity: 1;
pointer-events: auto;
transform: scale(1);
}
.modal-exit {
opacity: 0;
transform: scale(0.4);
}
...
现在,演示模态的用户界面的打开和关闭显得更加平滑,这是在不影响DOM负载的情况下实现的。
用CSSTransition 构建的模态。
总结
在这篇文章中,我们用一个React Portal模态的例子展示了React Portals的功能。然而,React Portals的应用并不仅限于模态或覆盖。我们还可以利用React Portals在包装层的一切之上渲染一个组件。
通过用ReactPortal 来包装组件的JSX或组件本身,我们可以跳过默认的DOM层次行为,在任何组件上获得React Portals的好处。
import ReactPortal from "./path/to/ReactPortal";
function AnyComponent() {
return (
<ReactPortal wrapperId="dedicated-container-id-if-any">
{/* compontents JSX to render */}
</ReactPortal>
);
}
这就是目前的全部内容!你可以在这个GitHub repo中找到本文的最终组件和样式,并在这里访问最终的[ReactPortal] 和模态组件的操作。
谢谢你的阅读。我希望你觉得这篇文章对你有帮助。请与其他人分享,他们可能会发现它的好处。Ciao!
The postBuilding a modal in React with React Portalsappeared first onLogRocket Blog.