前言
Modal (模态框) 是 web 开发中十分常见的组件,即从页面中弹出的对话框。
今天我们一起来用 React Hook 手写 Modal 模态框组件.
最终实现的效果codePen如下:
环境准备
本文代码在 create-react-app 脚手架生成的项目中运行,react 版本:
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-scripts": "3.3.0"
相应的直接引入 js 的方法,在 codePen 中可看得到全部代码。
从使用组件开始
使用 Modal 组件,最重要就是控制它的打开和关闭。我们先定义一个 modalVisible 的 state,当 modalVisible 为 true 时,模态框打开,反之关闭。
接下来就可以把 modalVisible 传入 Modal 组件来控制模态框的打开和关闭,同时 Modal 组件需要接收一个 onClose 的 prop,用来实现在组件中关闭模态框(例如点击蒙层时关闭模态框)。
const [modalVisible, setModalVisible] = useState(false);
const modalConfig = {
visible: modalVisible,
closeDialog: () => {
setModalVisible(false);
}
};
<Modal {...modalConfig}></Modal>
Modal 组件应具备良好的扩展性,做到可自定义模态框的内容(例如模态框的标题、关闭按钮、确定按钮等等)。我们把这部分自定义内容通通传入组件中。举个例子:
const modalChildren = (
<div className="dialog">
<span onClick={() => setModalVisible(false)} className="closeBtn">x</span>
<div>这是内容</div>
</div>
);
<Modal {...modalConfig}>{modalChildren}</Modal>
这部分自定义内容含有一个内容框,关闭按钮和文字内容,可以给它们添加一下样式:
/* App.css */
.openBtn {
margin-top: 240px;
border: 1px solid dodgerblue;
}
.dialog {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: 30px 30px;
width: 200px;
height: 200px;
background-color: #fff;
border-radius: 8px;
}
.closeBtn {
position: absolute;
right: 10px;
top: 4px;
font-size: 21px;
}
完整的使用 Modal 组件的代码:
import React, { useState } from 'react';
import './App.css';
import Modal from './components/modal';
function App() {
const [modalVisible, setModalVisible] = useState(false);
const modalConfig = {
visible: modalVisible,
closeModal: () => {
setModalVisible(false);
}
};
const modalChildren = (
<div className="dialog">
<span onClick={() => setModalVisible(false)} className="closeBtn">x</span>
<div>这是内容</div>
</div>
);
return (
<div className="App">
<button onClick={() => setModalVisible(true)} className="openBtn">open modal</button>
<Modal {...modalConfig}>{modalChildren}</Modal>
</div>
);
}
export default App;
编写 Modal 组件
清楚 Modal 组件需要接收的 props 之后,我们就可以开始编写组件了。
首先,我们需要给 modal 组件增加一个蒙层:
/* modal.css */
.modal {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 99;
}
开始编写组件:
import React from 'react';
import './css/modal.css';
const Modal = (props) => {
const { children, visible, closeModal } = props;
function handleClick(event) {
// 点击蒙层本身时关闭模态框,点击模态框的内容时不关闭
if (event.target === event.currentTarget) {
closeModal();
}
}
const modal = (
<div className="modal" onClick={handleClick}>
{children}
</div>
);
return <div>{visible && modal}</div>;
};
export default React.memo(Modal);
上面我们实现了通过visible来控制打开和关闭模态框,以及点击蒙层时关闭模态框。
接下来,我们要把模态框组件挂载在 body 的第一层中,而不是将模态框放置到父组件中。
因为模态框放置到父组件中很容易受到其他元素的干扰,仅是设置各个元素的 z-index 就使得代码难以维护。
我们可以使用 React 的 Portal 来实现把模态框组件应该挂载在 body 上:
import { createPortal } from 'react-dom';
createPortal(/* 组件内容 */, document.body)
Modal 组件完整代码:
import React from 'react';
import './css/modal.css';
import { createPortal } from 'react-dom';`
const Modal = (props) => {
const { children, visible, closeModal } = props;
function handleClick(event) {
// 点击蒙层本身时关闭模态框,点击模态框的内容时不关闭
if (event.target === event.currentTarget) {
closeModal();
}
}
const modal = createPortal(
<div className="modal" onClick={handleClick}>
{children}
</div>,
document.body
);
return <div>{visible && modal}</div>;
};
export default React.memo(Modal);
这样我们就完整地开发出一个简单的易扩展的 Modal 组件了,其实它就是一个蒙层,内容都是通过父组件传进来。
阻止背景滚动
当我们完成上面的编码之后,我们的模态框就可以实现显示/隐藏,并且处于 body 的顶层,但是还有一个问题,那就是如果 body 内容太长出现滚动时,滚动鼠标就会发现,模态框后边的背景也在滚动,这显然不是我们期望的效果。如何应对这种情况呢?
解决办法很巧妙,就是在模态框打开时,我们给 body 添加一个 overflow: hidden 的样式让 body 不滚动,关闭模态框时再去除这个样式。通过这样的方式,我们就实现在模态框打开时背景不滚动的效果了。
明白了原理之后就开始修改代码了,我们首先 在modal 组件中添加一个 bodyOverflow 的 useRef,用来保存 body 原始的 overflow 值,然后通过组件传入的 visible 来动态修改 body 的 overflow 值。
import React, { useEffect, useRef } from 'react';
import './css/modal.css';
import { createPortal } from 'react-dom';
const Modal = (props) => {
const { children, visible, closeModal } = props;
// 在第一次渲染时取 body 原始的 overflow 值
const bodyOverflow = useRef(window.getComputedStyle(document.body).overflow);
useEffect(() => { // 根据 visible 来动态修改 body 的 overflow 值
if (visible) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = bodyOverflow.current;
}
}, [visible]);
function handleClick(event) {
// 点击蒙层本身时关闭模态框,点击模态框的内容时不关闭
if (event.target === event.currentTarget) {
closeModal();
}
}
useEffect(() => {
// 组件销毁时恢复 body 的 overflow 值
return () => {
document.body.style.overflow = bodyOverflow.current;
}
}, []);
const modal = createPortal(
<div className="modal" onClick={handleClick}>
{children}
</div>,
document.body
);
return <div>{visible && modal}</div>;
};
export default React.memo(Modal);
打开模态框时效果如下:
扩展思考
有了这样一个灵活的 Modal 组件,我们可以用来做些什么呢?
在实际的开发中,经常需要用到的是一些公共的对话框组件,例如带标题、确定按钮、取消按钮和关闭按钮的弹框,又例如用于点击删除时需要弹出来的温馨提示弹框。
我们完全可以在 Modal 组件的基础上,封装这些对话框组件。如果让你实现以下的对话框组件,你会怎么做呢?赶紧来试试吧:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。