react 优雅实现多弹窗的需求
提出问题
现在有这样一个需求:存在 A
,B
,C
,D
四个弹窗,并且要在1
,2
,3
,4
, 5
等页面分别打开其中的几个,并在关闭时调用函数,该怎么去做呢?
例子:
这些模态框需要在
分别打开其中的几个并在关闭时触发不同的回调函数。
常规解法
一般ui库中,如ant-design, material-ui提供的Modal组件中都有一个控制开关的visible 或者open属性。
我们处理上面需求的时候。如果Component1 用到了ModalA, ModalB, Modal,这时候会在Component1中新建3个state,分别对应三个弹窗的开关状态
import ModalA from 'ModalA'
import ModalB from 'ModalB'
import modalC from 'ModalC'
export default function Component1() {
const [avisible, setavisible] = useState(false)
const [bvisible, setbvisible] = useState(false)
const [cvisible, setcvisible] = useState(false)
const handleCloseA = () => {
setavisible(false)
}
const handleCloseB() => {
setavisible(false)
}
const handleCloseC= () => {
setavisible(false)
}
return (
)
}
首先,观察以上代码,我们为了在Component1
中复用三个弹窗,多了三个visible
属性, 并且因为Modal是独立的组件,我们还要多三个打开的函数,三个关闭的函数。这样我们用到一个弹窗,就需要多一个state,多两个维护state的函数,对当前Component1
组件可以说是很不友好,且繁琐,
如果继续在Component2
, Component3 ...
中继续这样复用弹窗 无疑是一场小灾难。
解决方案1
构造一个open
方法,一个新的div
append到body内最下方可以写出modal.js
这个文件:
// modal.js
import ReactDom from 'react-dom'
export function open (ModalCom, props) {
const $div = document.createElement('DIV');
document.body.appendChild($div)
function close () {
ReactDom.unmountComponentAtNode($div)
document.removeChild($div)
}
// 将弹窗组件通过
ReactDom.render(
,
$div)
return close
}
这样我们就可以在Component1
这样去使用ModalA,B,C
:
import {open} from 'modal.js'
import ModalA from 'ModalA'
// ...
export function Component1() {
const handleOpenModalA = () => {
const close = open(ModalA, {
onModalAMissionDone: () => message.success('success')
})
}
const handleOpenModalB = () => {
const close = open(ModalB, {
onModalBMissionDone: () => message.success('success')
})
}
const handleOpenModalC = () => {
const close = open(ModalC, {
onModalCMissionDone: () => message.success('success')
})
}
return (
<>
>
)
}
乍一看,解决了维护三个state的问题,只需要三个open函数,传参,但却造成了其他的问题:
- return的
close
方法为局部变量,共享会使用全局变量 或者useRef
的方式,略微增加了复杂度。 - open方法调用多次,会打开多个相同的弹窗
- 每次close的时候,弹窗组件彻底销毁,性能开销过大
对于问题1,2 我们可以利用WeakMap等解决, 我们不妨相像我们的函数会返回一个背包一样的东西,open
的时候打开收集弹窗到背包里面, close
的时候从背包中拿出弹窗,关闭并删掉。并在一个文件中初始化背包,背包中的弹窗不能重复:
// modal.js
import ReactDom from 'react-dom'
export default generateModalBag() {
const mapClose = new WeakMap();
return {
open: (ModalCom, props) {
// 防止重复
if (mapClose.has(ModalCom)) return;
const $div = document.createElement('DIV');
document.body.appendChild($div)
function close () {
ReactDom.unmountComponentAtNode($div)
document.removeChild($div)
mapClose.delete($div)
}
ReactDom.render(
,
$div)
mapClose.set(ModalCom, close)
return close
},
close: (ModalCom){
const fun = mapClose.get(ModalCom)
if (typeof fun === 'function') fun()
}
}
}
于是在Component1
中我们就可以这样使用
import generateModalBag from 'modal.js'
import ModalA from 'ModalA'
const modal = generateModalBag()
export function Component1() {
const handleOpenModalA = () => {
modal.open(ModalA, {
onModalAMissionDone: () => message.success('success')
})
}
const handleCloseModalA = () => {
modal.close(ModalA)
}
return (
<>
>
)
}
这样我们就解决了问题1,2 。并且不会重复打开moalA弹窗, 却无法解决问题3,有没有更好的解决问题3的方案呢?那我们必须推倒这个方案重新设计
方案2: 利用高阶组件实现模态框注入
我们可以将visible属性放入高阶组件去进行维护,因为弹窗是多处公用的,可以看作是1,2,3等组件的依赖,我们可以采用注入的方式。根据modal动态创建state,并暴露modal构造函数映射 close,open方法的map给需要使用modal的组件:
export default function injectModal(...modals) {
return (WrapCom) => {
function ModalContainer() {
const [visibles, setVisibles] = useState(new Array(modals.length)).fill(false));
const [modalsProps, setmodalsProps] = useState(new Array(modals.length)).fill({}))
return (
<>
{modals.map((Modal, idx) => {
return (
)
}}
>
)
}
ModalContainer.displayName="InjectModalContainer"
return ModalContainer
}
}
我们可以将moalBag作为props传递给Component1
对弹窗进行管理,这样在Component1
中就可以这样使用ModalA, ModalB, ModalC
import injectModal from 'injectModal'
function ComponentA({modalBag}) {
const handleOpenA = () => {
const customProps = {} // 自定义props
modals.find(ModalA).open(customProps)
}
const closeModalA = () => {
const customProps = {} // 自定义props
modals.find(ModalA).close()
}
return (
)
}
export default injectModal(ModalA, ModalB, ModalC)(ComponentA)
这样就解决了上面提到的1,2,3三个问题,open,close方法在高阶组件中调用动态创建的对应的vsisible state的改变,不需要自己去维护开关state,不需要创建多余的dom,可以实现关闭时不销毁弹窗,可以适应第三方库,还可以自己构造useOpen useClose Hooks
或者componentDidOpen componentDidClose 生命周期
处理弹窗中的一系列行为。稍微改造一下我们甚至可以注入其他组件,当然这里唯一需要注意的就是处理ref
的行为
感谢。