现状
在平时的react
开发中,各种浮层(Modal
、Drawer
)是我们必不可少的交互之一。拿Modal
组件来举例子,如果做的是中后台系统,那下面这个页面应该比较熟悉
下面这种写法可能是大多数人一开始的写法,首先创建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官网对于高阶组件的解释。首先是一个函数,其次是参数为组件,返回值为新组件的函数。
既然知道了想要的需求,下面就可以开始实现了。首先定义一个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里面。
新的问题
虽然利用的高阶组件的思想,解决了之前提出的两个痛点,但是却带来了新的问题
- 浮层的打开和关闭都会重新渲染
dom
和销毁dom
,性能上不太优雅。 - 因为代码里面写死了
appendChild
到body
下面,导致浮层无法挂在到当前父组件下面 - 无法拿到子组件的ref
最后
性能上的问题,平时在项目里面感受来看,是在接受范围之内(好像没有感觉到什么明显的变化)。无法挂在到当前父组件下面...这个只能不用wrapper函数包裹了。无法拿到子组件的ref,react里面也是不建议去通过ref操作真实dom,目前还没遇到有浮层的场景必须要ref不可的场景,如果有,也只能不用wrapper函数了。
欢迎留言分享你们的方法,互相学习,一起进步。