一次說清,為什麼在Antd Modal中調resetFields調了個寂寞

Denzel發表於2021-12-24

背景

在幹了大半年增刪查改後(node,mysql,serverless),業務端人手短缺,老闆開恩讓我支援其他團隊寫幾個頁面。

久了不摸手生,除了react依稀記得,antd基本只能看著官方demo一行一行寫,感覺一天能寫完的,結果兩天了還沒聯調完。中間還遇到一些似曾相識的問題,可惜以前的經驗已經不管用了。

demo地址: https://codesandbox.io/s/antd...

這些問題在antd的倉庫issue都反覆被提及,看了下文,包括但不限於以下問題都將得到答案:
20211221224910

概括一下:

  • Form表單,React hooks 元件,initialValues初始化資料時候,第二次、第三次……傳遞新值,表單沒有更新,永遠顯示第一次資料?
  • 彈出層新建表單重新設定值不起作用?

    • Modal 用了destroyOnClose,裡面有 Form,並使用 form.resetFields,為什麼會失效?
    • Modal中initialValues更新了,使用了form.resetFields,要連續開啟兩次才生效?

有事說事

語言描述顯得太蒼白,所以直接看動圖吧:
reset-small

這是一個簡單的增刪查改頁面,新增和編輯共享了同一個元件,期望在開啟彈窗編輯表單關閉後,重新開啟時,能根據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的建立有一個非同步過程,所以子元件的渲染並不是同步的。正常的元件渲染是下面這樣的:

20211223222510

只需和我上面一樣,在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是個好東西,前提是你會用;
  • 我還是太菜了;

歡迎關注我的前端公眾號:前端黑洞

相關文章