背景
在幹了大半年增刪查改後(node,mysql,serverless),業務端人手短缺,老闆開恩讓我支援其他團隊寫幾個頁面。
久了不摸手生,除了react依稀記得,antd基本只能看著官方demo一行一行寫,感覺一天能寫完的,結果兩天了還沒聯調完。中間還遇到一些似曾相識的問題,可惜以前的經驗已經不管用了。
demo地址: https://codesandbox.io/s/antd...
這些問題在antd的倉庫issue都反覆被提及,看了下文,包括但不限於以下問題都將得到答案:
概括一下:
- Form表單,React hooks 元件,initialValues初始化資料時候,第二次、第三次……傳遞新值,表單沒有更新,永遠顯示第一次資料?
彈出層新建表單重新設定值不起作用?
- Modal 用了destroyOnClose,裡面有 Form,並使用 form.resetFields,為什麼會失效?
- Modal中initialValues更新了,使用了form.resetFields,要連續開啟兩次才生效?
有事說事
語言描述顯得太蒼白,所以直接看動圖吧:
這是一個簡單的增刪查改頁面,新增和編輯共享了同一個元件,期望在開啟彈窗編輯表單關閉後,重新開啟時,能根據initialValues重新渲染表單, 但得到的結果是,第二次開啟,編輯框沒有重新整理.
實現的虛擬碼大致是這樣:
import React, { useEffect } from "react";
import { Modal, Form, Input, Button, Checkbox } from "antd";
export function EditModal(props) {
const { visible, onOk, onCancel, content = {} } = props;
const [form] = Form.useForm();
const isEdit = !!content.sort;
const handlSubmit = (close) => {
// 一些提交邏輯
};
useEffect(() => {
// setTimeout(() => {
form.resetFields();
// });
}, [content]);
return (
<Modal
title={`${isEdit ? "編輯" : "新建"}備註`}
visible={visible}
destroyOnClose
onOk={onOk}
onCancel={onCancel}
>
<Form
name="basic"
labelCol={{ span: 7 }}
wrapperCol={{ span: 14 }}
form={form}
initialValues={content}
autoComplete="off"
>
{...一些表單}
</Form>
</Modal>
);
}
相信出現問題的盆友們,大多都是和我一樣,如上面這樣的程式碼這樣實現。
具體問題,具體分析
先給結論,之所以會出現上面的那些問題,主要是三個問題導致:
- react hooks使用姿勢不正確,antd4 form引入了hooks, 和antd3使用有所區別;
- 對form表單initialValues的認識不清;
- Modal子元素的渲染是非同步的,destroyOnClose 錯誤使用;
initialValues初始化資料時候,第二次、第三次……傳遞新值,表單沒有更新?
因為initialValues只在表單首次初始化時有效,只要表單沒有解除安裝並重新掛載,改變initialValues都不會重新整理表單的值,form最初的設計就如此;以下是initialValues初始化存到store的完整實現:
this.setInitialValues = function (initialValues, init) {
_this.initialValues = initialValues || {};
if (init) {
// setValues 作用類似於Object.assign();
_this.store = setValues({}, initialValues, _this.store);
}
};
this.store 是存放在form例項中的,只要例項不銷燬,store的值就不會變化。
destroyOnClose,彈出層新建表單重新設定值不起作用?
首先這裡有個概念,initialValues 在Form表單例項掛載時,這個值是被存在了用hooks生成的form例項中。
所以當我們使用了destroyOnClose,雖然銷燬了Modal 以及Modal框中的Form,但這個form例項仍然存在,這個hook例項是掛載在EditModal元素上的,並沒有被一起銷燬,所以當彈窗再次開啟,Form表單又會根據這個form的store再次渲染(原因見上)。
Modal 用了destroyOnClose,裡面有 Form,並使用 form.resetFields,為什麼會失效?
當我們意識到form例項沒有被銷燬,可能儲存了上一個表單編輯狀態時,我們會想到使用useEffect鉤子,去觀察初始值,採用form.resetFields去重置例項,但最後發現這並沒有起作用(我也踩到了這個坑上)。
當我去掉destroyOnClose,我發現生效了,後面我去看了一下form.resetFields的實現原始碼:
this.resetFields = function (nameList) {
var prevStore = _this.store;
if (!nameList) {
// console.log(JSON.stringify(prevStore), JSON.stringify(_this.initialValues));
_this.store = setValues({}, _this.initialValues);
_this.resetWithFieldInitialValue();
_this.notifyObservers(prevStore, null, {
type: 'reset'
});
return;
}
}
這個實現和initialValues 一樣簡單明瞭,所以問題不在resetFields。問題是出在Modal身上,簡單來講Moda的建立有一個非同步過程,所以子元件的渲染並不是同步的。正常的元件渲染是下面這樣的:
只需和我上面一樣,在resetFields加一句console, 就會發現_this.initialValues是上一次的初始值,而不是新傳入的(因為Form元素還未掛載),所以這裡resetFields調了個寂寞。
還有一種簡單的方法證明Modal元件的子元件掛載是非同步的,就是如下面這樣去玩:
useEffect(() => {
setTimeout(() => {
form.resetFields();
});
}, [content]);
這個實現,你會發現resetFields居然生效了,因為一個巨集任務後,Form元素已經掛載上。
所以這裡告訴我們,要儘量少用destroyOnClose,因為Modal的渲染是耗時的且費力的。
Modal使用了form.resetFields初始化,要連續開啟兩次才生效?
相信經過上面的一系列解釋,你的心中已經有了答案;destroyOnClose 確實不適合在Modal中寫表單時用。
所以,Modal中重置Form initalValues的正確姿勢了嗎?
吃一塹,長一智
這一次經歷後,我記住了:
- destroyOnClose要慎用,因為Modal的渲染是昂貴的;
- hooks 是個好東西,但你得用對;
- antd是個好東西,前提是你會用;
- 我還是太菜了;
歡迎關注我的前端公眾號:前端黑洞