React Hook 手写 Modal 模态框组件

前言

Modal (模态框) 是 web 开发中十分常见的组件,即从页面中弹出的对话框。
今天咱们一块儿来用 React Hook 手写 Modal 模态框组件,最终实现的效果以下:
modal.pngcss

环境准备

本文代码在 create-react-app 脚手架生成的项目中运行,react 版本:html

"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-scripts": "3.3.0"

从使用组件开始

使用 Modal 组件,最重要就是控制它的打开和关闭。咱们先定义一个 modalVisible 的 state,当 modalVisible 为 true 时,模态框打开,反之关闭。
接下来就能够把 modalVisible 传入 Modal 组件来控制模态框的打开和关闭,同时 Modal 组件须要接收一个 onClose 的 prop,用来实如今组件中关闭模态框(例如点击蒙层时关闭模态框)。react

const [modalVisible, setModalVisible] = useState(false);
const modalConfig = {
    visible: modalVisible,
    closeDialog: () => {
        setModalVisible(false);
    }
};
<Modal {...modalConfig}></Modal>

Modal 组件应具有良好的扩展性,作到可自定义模态框的内容,例如模态框的标题、关闭按钮、肯定按钮等到。咱们把这部分自定义内容统统传入组件中。举个例子:web

const modalChildren = (
    <div className="dialog">
        <span onClick={() => setModalVisible(false)} className="closeBtn">x</span>
        <div>这是内容</div>
    </div>
);

<Modal {...modalConfig}>{modalChildren}</Modal>

这部分自定义内容含有一个内容框,关闭按钮和文字内容,能够给它们添加一下样式:segmentfault

/* 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 组件的代码:app

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 以后,咱们就能够开始编写组件了。dom

首先,咱们须要给 modal 组件增长一个蒙层:编码

/* modal.css */
.modal {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    background: rgba(0, 0, 0, 0.3);
    z-index: 99;
}

开始编写组件:spa

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来控制打开和关闭模态框,以及点击蒙层时关闭模态框。code

接下来,咱们要把模态框组件挂载在 body 的第一层中,而不是将模态框放置到父组件中。
由于模态框放置到父组件中很容易受到其余元素的干扰,仅是设置各个元素的 z-index 就使得代码难以维护。

咱们能够使用 React 的 Portal 来实现把模态框组件应该挂载在 body 上:

import { createPortal } from 'react-dom';
createPortal(/* 组件内容 */, document.body)

modal2.png

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 的 state,用来保存 body 原始的 overflow 值,而后经过组件传入的 visible 来动态修改 body 的 overflow 值。

import React, { useEffect, useState } from 'react';
import './css/modal.css';
import { createPortal } from 'react-dom';

const Modal = (props) => {
    const { children, visible, closeModal } = props;
    
    const [bodyOverflow, setBodyOverflow] = useState(false);

    useEffect(() => { // 在第一次渲染时取 body 原始的 overflow 值
        setBodyOverflow(window.getComputedStyle(document.body).overflow);
    }, [])

    useEffect(() => { // 根据 visible 来动态修改 body 的 overflow 值
        if (visible) {
            document.body.style.overflow = 'hidden';
        } else {
            document.body.style.overflow = bodyOverflow;
        }
    }, [visible, bodyOverflow])

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

打开模态框时效果以下:
modal3.png

参考文章

相关文章
相关标签/搜索