React 最佳实践:如何实现原生对话框(Portals)

1,786 阅读2分钟

React 实现对话框有两种方案

  • 使用 UI 组件库:比如 Antd 的组件 -- Modal
  • 原生 React Portals

dialog-1.png 在实际开发中,我们大多会选择直接使用 Antd 的对话框组件,其实 Antd 对话框的实现也是基于 React Portals 这个特性,所以了解它可以让我们的漂浮层之路走的更宽哦:

dialog-2.png

dialog-3.png

React Portals 是什么?

Portal 是 React 16.3 新引入的 API,React 是在内存中维护了一棵 Virtual Dom Tree ,这棵树会映射到真实的 Dom 节点,而 Portal 可以打断这种映射关系,它提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案,一举解决了漂浮层的问题,如:Dialog、Tooltip 等。

用法

ReactDOM.createPortal(child, container);

第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。

需要注意,虽然 child 看似显示在另一颗 Dom 树上,但在 Virtual Dom 中,其实是同一棵,在下面的例子中也会看到。

Antd 的 Dialog

按照官网文档,上手非常简单

import React, { useState } from 'react';
import { Modal, Button } from 'antd';

const DialogPage = () => {
  const [isModalVisible, setIsModalVisible] = useState(false);

  const showModal = () => {
    setIsModalVisible(true);
  };

  const handleOk = () => {
    setIsModalVisible(false);
  };

  const handleCancel = () => {
    setIsModalVisible(false);
  };

  return (
    <>
      <Button type="primary" onClick={showModal}>
        Open Antd Modal
      </Button>
      <Modal
        title="Basic Modal"
        visible={isModalVisible}
        onOk={handleOk}
        onCancel={handleCancel}
      >
        <p>Some contents...</p>
        <p>Some contents...</p>
        <p>Some contents...</p>
      </Modal>
    </>
  );
};

export default DialogPage;

Portals 的 Dialog

在 index.html 中,创建一个 dialog 的容器元素:

<div id="root"></div>
<div id="dialog-root"></div>

通过组件内部状态(visible) 控制 Dialog 的渲染:

{
  visible
    ? createPortal(
        <div className="portal-sample">
          {children}
          <Button onClick={onHide}>close</Button>
        </div>,
        document.getElementById('dialog-root'),
      )
    : null;
}

从 React Components 可以看出来,PortalDialog 和 DialogPage 的父子关系依然存在:

dialog-5.png

完整代码

pages/dialog.js

import React, { useState } from 'react';
import { Button } from 'antd';
import PortalDialog from '@/components/PortalDialog';

const DialogPage = () => {
  const [isPortalVisible, setIsPortalVisible] = useState(false);

  const showPortal = () => {
    setIsPortalVisible(true);
  };

  const hidePortal = () => {
    setIsPortalVisible(false);
  };

  return (
    <>
      <Button style={{ marginLeft: '20px' }} onClick={showPortal}>
        Open Dialog(React Portals)
      </Button>
      <PortalDialog visible={isPortalVisible} onHide={hidePortal}>
        <div>dialog-children</div>
      </PortalDialog>
    </>
  );
};

export default DialogPage;

components/PortalDialog/index.js

import { createPortal } from 'react-dom';
import { Button } from 'antd';
import './style.css';

const PortalDialog = (props) => {
  const { visible, children, onHide } = props;
  return visible
    ? createPortal(
        <div className="portal-sample">
          {children}
          <Button onClick={onHide}>close</Button>
        </div>,
        document.getElementById('dialog-root'),
      )
    : null;
};

export default PortalDialog;

components/PortalDialog/style.css

.portal-sample {
  position: absolute;
  padding: 20px;
  width: 500px;
  height: 300px;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  background-color: #fff;
  border-radius: 10px;
  border: 1px solid #ddd;
  box-shadow: 0px 0px 20px 2px #ddd;
}

React 最佳实践