因业务需要需要在Modal中实现表单的回显,最开始使用了如下方法
js
const initialValues = useRef({});
....
useEffect(() => {
if (!id) return;
setLoading(true);
getTreatMethod({ id })
.then((res) => {
initialValues.current = {
...res,
inStatusDtos: res.hospitalizationStatusDtos.map((item) => {
return item.id.toString();
}),
};
setLoading(false);
})
.catch((e) => {
message.error((e as Error).message);
setLoading(false);
});
}, [id]);
<Form
initialValues={initialValues.current}
name="basic"
form={form}
onFinish={onFinish}
layout="vertical"
className={styles.form}
disabled={status === DrawerStatus.Check}
>...
</Form>
但是发现用initialValues来进行回显,表单的值始终为上一次表单的值。
在文档中发现如下说明
<Modal />
默认关闭后状态不会自动清空,如果希望每次打开都是新内容,请设置destroyOnClose
。<Modal />
和 Form 一起配合使用时,设置destroyOnClose
也不会在 Modal 关闭时销毁表单字段数据,需要设置<Form preserve={false} />
。
根据文档在Form和Modal中添加了对应的字段,却并没有解决问题。
于是开始从头又看了一遍代码,怀疑是不是因为在设置关闭Modal后表单并没有重新渲染。于是又添加了如下代码
js
useEffect(() => {
form.resetFields();
}, [open]);
发现仍未解决问题,突然想起来Form表单回显可以用setFieldsValue

将代码改为
js
useEffect(() => {
if (!id) return;
setLoading(true);
getTreatMethod({ id })
.then((res) => {
form.setFieldsValue({
...res,
inStatusDtos: res.hospitalizationStatusDtos.map((item) => {
return item.id.toString();
}),
});
setLoading(false);
})
.catch((e) => {
message.error((e as Error).message);
setLoading(false);
});
}, [id]);
解决问题
在修改代码中间几个小时一直在思考究竟是为什么会出现这个错误,一直没有想到为什么,现在回过头去梳理的时候突然发现多半是因为antd表单initialValues初始化时机的问题。
Form在Modal打开时就进行了初始化,在执行getTreatMethod()拿到res给initialValues赋值时已经完成了表单初始化,而且因为是使用ref去记录并不会对组件进行重新渲染,也解释了为什么始终表单的值始终是上一个表单,而不是当前表单,出现了闭包陷阱。
回顾整个问题,发现起因是我对Ref的滥用以及掌握程度不深。在没有涉及到Modal和Drawer的表单都是用ref去储存initialValues并未发现此问题,总觉得使用ref就不会出现这种闭包问题。而且最关键的是所有回显都是用initialValues而忽略了form的方法setFieldsValue从而造成了这个bug。
总结
- Antd表单回显有两个方法initialValues和form.setFieldsValue(),一个有问题就换另一个
- 在涉及Modal与Drawer的表单回显直接用form.setFieldsValue()
- 不要滥用ref,不要觉得只要用了ref就不会出现闭包陷阱