利用高阶组件(HOC)思想实现更优雅的modal框

2,909 阅读4分钟

现状

在平时的react开发中,各种浮层(ModalDrawer)是我们必不可少的交互之一。拿Modal组件来举例子,如果做的是中后台系统,那下面这个页面应该比较熟悉

image.png

下面这种写法可能是大多数人一开始的写法,首先创建index.js文件,是由Table + Button组件构成的。

import React, { useState } from 'react';
import { Table, Button, Divider} from 'antd';
import OpenModal from './openModal';
import ReactDOM from 'react-dom';
import './index.css';

const data = [{
  name: '张三',
  age: '12'
}, {
  name: 'tom',
  age: '22'
}]

const App = () => {
  const [visible, setVisible] = useState(false);
  const [record, setRecord] = useState({});
  const columns = [
    {
      dataIndex: 'name',
      title: '姓名'
    },{
      dataIndex: 'age',
      title: '年龄'
    }, {
      title: '操作',
      render: (text, record) => {
        return <div>
          <a onClick={() => showModal(record)}>编辑</a>
          <Divider type="vertical" />
          <a >查看</a>
          <Divider type="vertical" />
          <a >删除</a>
        </div>
      }
    }
  ]

  const showModal = (record) => {
    setVisible(true);
    if (record) {
      setRecord(record)
    }
  };

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

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

  return (
    <>
      <Button type="primary" onClick={showModal}>
        添加人员
      </Button>
      <Table
        dataSource={data}
        columns={columns}
      />
      <OpenModal
        handleOk={handleOk}
        handleCancel={handleCancel}
        visible={visible}
        setVisible={setVisible}
        record={record}
      />
    </>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));

然后创建openModal.js,里面存放子组件modal:

import React from 'react';
import {Modal} from 'antd';

const MemberModal = (props) => {
  return <Modal
    title={'新建'}
    visible={props.visible}
    onCancel={props.handleCancel}
    onOk={props.handleOk}
  >
    <p>Some contents...</p>
    <p>Some contents...</p>
    <p>Some contents...</p>
  </Modal>
};

export default MemberModal;

痛点

这种写法的话有几个痛点:

  • 如果父组件页面交互比较复杂,有很多modal需要打开,你可能需要为每一个modal都设置一个visible和对应的setVisible函数来分别控制它们。
  • Table组件里面有编辑功能,打开编辑的modal框,需要将当前行数据(record)传到modal里面,大多数的做法是在编辑函数里面将record存到state里面,然后在父组件的render函数里面将这个state传到modal组件里面。

上面这两点无疑都额外的增加了state变量,间接的使代码的可读性变低;而且同一个组件内,如果modal有3个以上,不采取一些措施的话,代码的可读性将会变得更加糟糕。

解决

对于第一个痛点,可以抽取重复逻辑visible变量和setVisible函数;对于第二个痛点,如果能够将浮层组件渲染的时间控制在编辑函数的里面,那就不用state里面存一份record了。

很自然的想到了高阶组件。看一下react官网对于高阶组件的解释。首先是一个函数,其次是参数为组件,返回值为新组件的函数。

image.png

既然知道了想要的需求,下面就可以开始实现了。首先定义一个wrapper函数, 它接收一个组件,其次我希望在父组件触发打开浮层函数的地方就渲染modal,所以还需要返回一个渲染的方法,源码如下:

import React from 'react';
import ReactDOM from 'react-dom';

const wrapper = (component) => {
  // 销毁组件
  const destoryDialog = (element) => {
    const unmountResult = ReactDOM.unmountComponentAtNode(element);
    if(unmountResult && element.parentNode) {
      setTimeout(() => {
        element.parentNode.removeChild(element);
      }, 300);
    }
  }
  // 渲染组件
  const render = ({element, component, config}) => {
    const comInstance = React.createElement(component, {
      ...config,
      key: 'div',
      closeDialog: () => {
        destoryDialog(element)
      },
      visible: true
    })
    ReactDOM.render(comInstance, element)
  }
  return function (config) { // 挂载div
    const element = document.createElement('div');
    render({element, component, config});
    document.getElementsByTagName('body')[0].appendChild(element);
  }
};

export default wrapper;

使用起来也是很方便, 对于子组件modal,只需要引入这个wrapper函数即可

import React from 'react';
import {Modal} from 'antd';
+ import wrapper from 'xxxx';
const MemberModal = (props) => {
  return <Modal
    title={'新建'}
    visible={props.visible}
-   onCancel={props.handleCancel}
-   onOk={props.handleOk}
+   onCancel={props.closeDialog}
+   onOk={props.closeDialog}
  >
    <p>Some contents...</p>
    <p>Some contents...</p>
    <p>Some contents...</p>
  </Modal>
};
- export default MemberModal;
+ export default wrapper(MemberModal);

对于父组件,也需要修改一点点。

import React, { useState } from 'react';
import { Table, Button, Divider} from 'antd';
import MemberModal from './openModal';
import ReactDOM from 'react-dom';
import './index.css';

const data = [{
  name: '张三',
  age: '12'
}, {
  name: 'tom',
  age: '22'
}]

const App = () => {
-  const [visible, setVisible] = useState(false);
-  const [record, setRecord] = useState({});
  const columns = [
    {
      dataIndex: 'name',
      title: '姓名',
      width: '40%'
    },{
      dataIndex: 'age',
      title: '年龄',
      width: '40%'
    }, {
      title: '操作',
      render: (text, record) => {
        return <div>
+          <a onClick={() => showModal('edit', record)}>编辑</a>
-         <a onClick={() => showModal(record)}>编辑</a>
          <Divider type="vertical" />
          <a >查看</a>
          <Divider type="vertical" />
          <a >删除</a>
        </div>
      }
    }
  ]

  const showModal = (type, record = {}) => {
+    MemberModal({
+      type,
+      record,
+    })
-    setVisible(true);
-    if (record) {
-      setRecord(record)
-    }
  };

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

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

  return (
    <>
      <Button
        style={{float: 'right', marginBottom: '12px'}}
        type="primary"
+       onClick={() => showModal('add')}
-       onClick={showModal}
      >
        添加人员
      </Button>
      <Table
        rowKey={(record) => record.name}
        dataSource={data}
        columns={columns}
      />
-      <MemberModal
-        handleOk={handleOk}
-        handleCancel={handleCancel}
-        visible={visible}
-        setVisible={setVisible}
-        record={record}
-      />
    </>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));

分别打开新增按钮和编辑按钮,打印子组件的props,我们可以通过type值的不同来判断打开的是新增操作还是编辑操作,从而做一些逻辑处理,非常的方便;而且在父组件内,也不需要再将record之类的变量存到state里面。 image.png

新的问题

虽然利用的高阶组件的思想,解决了之前提出的两个痛点,但是却带来了新的问题

  • 浮层的打开和关闭都会重新渲染dom和销毁dom,性能上不太优雅。
  • 因为代码里面写死了appendChildbody下面,导致浮层无法挂在到当前父组件下面
  • 无法拿到子组件的ref

最后

性能上的问题,平时在项目里面感受来看,是在接受范围之内(好像没有感觉到什么明显的变化)。无法挂在到当前父组件下面...这个只能不用wrapper函数包裹了。无法拿到子组件的ref,react里面也是不建议去通过ref操作真实dom,目前还没遇到有浮层的场景必须要ref不可的场景,如果有,也只能不用wrapper函数了。

欢迎留言分享你们的方法,互相学习,一起进步。