如何优雅的使用ant.design的Modal组件?
一个数据列表少不了数据的增、删、改,一般我们都是通过Modal+Form实现表单数据的获取,在一个复杂的数据列表中我们需要好多个Modal+Form的配合,这时候我们需要维护好多个Modal的visible状态,例如下面的场景:
const List = () => {
const [visible1, setVisible1] = useState(false);
const [visible2, setVisible2] = useState(false);
const [visible3, setVisible3] = useState(false);
const [visible4, setVisible4] = useState(false);
const [visible5, setVisible5] = useState(false);
const [visible6, setVisible6] = useState(false);
return(
<div>
<Modal visible={visible1}><Form1/></Modal>
<Modal visible={visible2}><Form2/></Modal>
<Modal visible={visible3}><Form3/></Modal>
<Modal visible={visible4}><Form4/></Modal>
<Modal visible={visible5}><Form5/></Modal>
<Modal visible={visible6}><Form6/></Modal>
<div/>
)
}
姑且不算其他代码就Modal和state我们就写了将近20行代码,那如何减少Modal和state的维护呢?
封装useModal实现Modal优雅的使用:
首先,我们看一下预期最简单的使用方式,我们只需要调用modal.open方法时,替换children就可达到Modal重复使用。
export default () => {
const [modal, ModalDOM] = useModal();
const handleClick = () => {
modal.open({
title: '收集数据',
children: <Form1/>,
onOk: (values: any) => { // values为收集到的表单d
modal.close();
}
});
}
return (
<>
<button onClick={handleClick}>自定义useModal</button>
{ModalDOM}
</>
)
}
上面我们写的预期效果,我们知道useModal返回了一个钩子和一个Modal组件,通过调用钩子的open方法和close方法来控制Modal组件打开和关闭,下面我们通过预期,逆向实现主体函数:
interface modalRefType {
open: () => void;
close: () => void;
injectChildren: (child: React.ReactElement) => void;
injectModalProps: (props: ModalProps) => void;
}
export default () => {
const modalRef = useRef<modalRefType>();
const handle = useMemo(() => {
return {
open: ({ children, ...rest }) => {
modalRef.current.injectChildren(children); // 注入子组件
modalRef.current.injectModalProps(rest); // 注入Modal的参数
modalRef.current.open();
},
close: () => {
modalRef.current.close();
}
};
}, []);
return [handle, <MyModal ref={modalRef} />] as const;
}
主体函数思路还是比较清晰,通过handle+useRef控制MyModal组件暴露出来的injectChildren、injectModalProps、open、close方法,下面我们再根据暴露出来的这些方法,逆向编程实现MyModal组件:
const MyModal = memo(forwardRef((prop: any, ref) => {
const [form] = Form.useForm();
const [modalChildren, setModalChildren] = useState<React.ReactElement>(null);
const [modalProps, setModalProps] = useState<ModalProps>({
visible: false,
});
// ant.design 4.0 Form的onFinish触发回调
const onFinish = useCallback((values: any) => {
modalProps.onOk?.(values);
}, [form, modalProps]);
// 关闭当前Modal
const onClose = useCallback(() => {
setModalProps((source) => ({
...source,
visible: false,
}));
}, [form]);
// 关闭当前Modal
const onOpen = useCallback(() => {
setModalProps((source) => ({
...source,
visible: true,
}));
}, [form]);
useImperativeHandle(ref, () => ({
// 注入Modal的子组件
injectChildren: (element) => {
setModalChildren(element);
},
// 注入Modal参数
injectModalProps: (props) => {
console.log(props)
setModalProps((source) => {
return {
...source,
...props,
}
});
},
// 打开Modal
open: () => {
onOpen();
},
// 关闭Modal
close: () => {
onClose();
}
}), []);
// 这里的Modal是ant.design中的Modal
return (
<Modal
{...modalProps}
onCancel={onClose}
onOk={() => form.submit()}.
>
{
modalChildren
?
React.cloneElement(modalChildren, {
onFinish,
form,
onClose,
})
: null
}
</Modal>
)
}));
我们通过React.cloneElement克隆了子组件(就是我们的业务代码Form),并且注入form和onFinish实现对表单的控制,通过onOk方法的挟持,实现在点击Modal的ok按钮时获取表单数据。
以上代码有一个细节,我们在改变状态的时候都是使用函数的方式setModalProps((sourcce) => ({...source, ...})),这主要是为了获取最新的状态进行。
到目前为止,我们已经实现了useModal基础版本,它只能让我们创建表单,而无法编辑表单和脱离表单去使用。
进阶实现useModal
为了脱离Form使用和表单的编辑,我们添加了type、initialValues参数:
....
// 修改使用方式
const handleClick = () => {
modal.open({
title: '收集数据',
type: 'form', // 类型为form时需要Form触发onFinish才触发onOk
initialValues: { name: '原值' }, // 表单默认值
children: <Form1/>,
onOk: (values: any) => {
console.log('收集到的表单', values);
modal.close();
}
});
}
....
升级后useModal完整的代码:
import React, { useRef, useMemo, memo, forwardRef, useCallback, useState, useImperativeHandle } from 'react';
import { Modal, Form } from 'antd';
import type { ModalProps } from 'antd';
import "antd/dist/antd.css";
const MyModal = memo(forwardRef((prop: any, ref) => {
const [form] = Form.useForm();
const [modalChildren, setModalChildren] = useState<React.ReactElement>(null);
const [modalProps, setModalProps] = useState<ModalProps>({
visible: false,
});
const typeRef = useRef<string>();
// ant.design 4.0 Form的onFinish触发回调
const onFinish = useCallback((values: any) => {
modalProps.onOk?.(values);
}, [form, modalProps]);
// 关闭当前Modal
const onClose = useCallback(() => {
setModalProps((source) => ({
...source,
visible: false,
}));
}, [form]);
// 关闭当前Modal
const onOpen = useCallback(() => {
setModalProps((source) => ({
...source,
visible: true,
}));
}, [form]);
useImperativeHandle(ref, () => ({
// 注入Modal的子组件
injectChildren: (element) => {
setModalChildren(element);
},
// 注入Modal参数
injectModalProps: (props) => {
console.log(props)
setModalProps((source) => {
return {
...source,
...props,
}
});
},
// 打开Modal
open: () => {
onOpen();
},
// 关闭Modal
close: () => {
onClose();
},
// 设置表单数据
setFieldsValue: (values: any) => {
form.setFieldsValue?.(values);
},
setType: (type: string) => {
typeRef.current = type;
}
}), []);
const handleOk = useCallback(() => {
if (typeRef.current === 'form') {
form.submit();
} else {
modalProps.onOk?.(null);
}
}, [form, modalProps]);
// 这里的Modal是ant.design中的Modal
return (
<Modal
{...modalProps}
onCancel={onClose}
onOk={handleOk}
>
{
modalChildren
?
React.cloneElement(modalChildren, {
onFinish,
form,
onClose,
})
: null
}
</Modal>
)
}));
interface modalRefType {
open: () => void;
close: () => void;
injectChildren: (child: React.ReactElement) => void;
injectModalProps: (props: ModalProps) => void;
setFieldsValue: (values: any) => void;
setType: (type: string) => void;
}
interface openArgType extends ModalProps {
children?: React.ReactElement,
type?: 'form' | 'default',
initialValues?: {
[key: string]: any;
},
}
export default () => {
const modalRef = useRef<modalRefType>();
const handle = useMemo(() => {
return {
open: ({ children, type, initialValues, ...rest }: openArgType) => {
modalRef.current.setType(type);
modalRef.current.injectChildren(children); // 注入子组件
modalRef.current.injectModalProps(rest); // 注入Modal的参数
modalRef.current.open();
if (initialValues && type === 'form') {
modalRef.current.setFieldsValue?.(initialValues);
}
},
close: () => {
modalRef.current.close();
}
};
}, []);
return [handle, <MyModal ref={modalRef} />] as const;
}
完结:以上我们通过了逆向的编程方式,实现了我们需求的useModal钩子,用这样的一个hook使用Modal是不是优雅多了。