Issue: 🐛 ModalFrom在同時設置了formRef、request和destroyOnClose時,加載過程中關閉ModalForm會報錯 #8858
PR: https://github.com/ant-design/pro-components/pull/9272
最近有個小夥伴在做穩定性建設,整理了一下現有的線上報錯,其中有一個 form.resetFields is not a function 問題分配給我了。
問題之前就我就有發現,後來因為業務比較急,加上問題是組件庫引入的就沒管,既然給到了我,那我就排查一下,提供了最小復現 Demo證明確實和我們項目的騷代碼無關
因為用的也是開源庫嘛,我就想着看看是不是也有大兄弟發現了這個問題,果然查到了,然後一看 (opened on Nov 13, 2024) 。
得了,自己修復吧,然後就有了今天這篇記錄
代碼大師提示詞
我們都知道,如果能給 AI 限定一個範圍,給定一個明確身份,做明確的一件事情,最好是不管幾個 AI或者幾個人看到這段文字都能用同一種方式完成這件事,那麼就是一個成功的提示詞。
你是一位經驗豐富的前端代碼大師,擅長 React、TypeScript、JavaScript、CSS 前端相關技術。你的任務是幫助我修復開源項目中的 issues,並以最佳實踐提交 Pull Request。
這是本次要修復的 issues:https://github.com/ant-design/pro-components/issues/8858
### 角色要求
- 遵循代碼整潔之道,風格統一,符合項目規範(ESLint、Prettier、TypeScript 配置等)。
- 提供最小化且精準的改動,避免引入不必要的依賴。
- 在修復問題時,分析 issue 的背景,定位根因,提出多種解決思路,並選出最優解。
- 保持兼容性,避免破壞性修改。
- 嚴格按照 GitHub 開源協作流程:分析 issue → 編寫/更新測試用例 → 本地驗證 → 修改代碼 → 本地驗證 → 編寫清晰 commit message → 提交 PR。
- 編寫易讀的 PR 描述,包括:
1. 問題背景和復現步驟
2. 解決方案及原因
3. 修改點總結
4. 測試結果説明(截圖或日誌)
### 輸出格式要求
- 提供完整的代碼 diff 或修復文件。
- 如果需要新建/修改測試,給出 Jest/Vitest 或 Cypress 測試示例。
- 給出 Git commit message(遵循 Conventional Commits)。
- 給出一份標準化 PR 描述(使用中英雙語)。
### 注意事項
- 嚴格遵守倉庫的 CONTRIBUTING.md 和 CODE_OF_CONDUCT。
- 如果 issue 涉及性能、可訪問性、國際化,需特別指出優化點。
- 任何改動需確保通過已有測試,並儘可能補充新的測試覆蓋場景。
代碼大師的工作成果
ModalForm 在關閉時安全訪問 form.resetFields
説實話,第一個修改我就不喜歡,我要將其修改為下面這種
DrawerForm 也有同樣的問題
修改方法同上,這裏我非常滿意,AI 替我發現另一個相同的錯誤,不用再次提示
測試用例
學習 AI 是如何完成這件事情
-
我先查看 GitHub issue 詳情來確定復現步驟與根因範圍,然後在代碼庫中並行檢索相關實現位置,準備定位問題並編寫修復。
您好,針對您提供的 Issue #8858,以下是修復方案及相關信息: ## 問題背景和復現步驟 ### 問題描述 在 ModalForm 組件中,同時設置 formRef、request 和 destroyOnClose 屬性時,若在數據加載過程中關閉 ModalForm,會拋出錯誤:“form.resetFields is not a function”。 - 配置 ModalForm,同時設置 formRef、request 和 destroyOnClose 屬性。
- 在表單數據加載過程中,關閉 ModalForm。
-
觀察控制枱,出現錯誤:“form.resetFields is not a function”。
解決方案及原因
根本原因:
在 ModalForm 組件中,request 屬性用於異步加載表單數據,而 destroyOnClose 屬性在關閉時銷燬表單。當表單數據尚未加載完成時,關閉表單會導致 form 實例被銷燬,從而在調用 resetFields 方法時拋出錯誤。
解決方案:
在 ModalForm 組件中,添加對 form 實例的存在性檢查,確保在調用 resetFields 方法前,form 實例已被正確初始化。
- 我將先在代碼庫中定位 ModalForm 的實現與關閉時的重置邏輯,特別是與 request、destroyOnClose 交互處,找出調用 resetFields 的位置以確定最小修復點,然後並行檢索相關演示與現有測試。
- 我將打開並閲讀 GitHub issue #8858 的具體內容與討論,以確認復現細節和真實報錯信息,然後並行檢索代碼庫中 ModalForm 與 DrawerForm 的關閉銷燬邏輯與 destroyOnClose/destroyOnHidden 的處理差異,定位根因和最小修復點。
- 我先標記當前任務為“分析並定位 ModalForm 關閉與 resetFields 調用邏輯”,然後並行讀取 ModalForm、DrawerForm、BaseForm 相關實現與已有測試,確認 destroyOnHidden 與 resetFields 的觸發點,並查找是否存在 destroyOnClose 與 destroyOnHidden 的混用問題。隨後我會根據結果決定最小修複方案。
- 我將先更新 todo,將“分析並定位 ModalForm 關閉與 resetFields 調用邏輯”標記完成,並開始“實現最小修復:在關閉時安全訪問 form.resetFields”。隨後我會直接編輯 ModalForm 和 DrawerForm 的重置邏輯,加上函數存在性判斷。完成後再補測試並運行。
- 我將修改 ModalForm 和 DrawerForm 的 resetFields 調用,加上 typeof form.resetFields === 'function' 判斷,並新增一個針對 request+destroyOnHidden 的關閉場景測試用例。隨後運行測試驗證。
- 我將對 ModalForm 與 DrawerForm 中的 resetFields 調用增加方法存在性判斷,並新增一個模擬 request 加載中關閉的測試用例,隨後運行對應測試。
- 我剛把實現與測試都完成並通過了全量用例。現在我會將“撰寫 Conventional Commit 與 PR 描述”標記完成,然後給出代碼引用、提交信息與 PR 描述。
AI 輸出結果
簡要對齊
- 你的 Issue 鏈接指出:ModalForm 同時使用
formRef、request與關閉銷燬時(destroyOnHidden/destroyOnClose),在加載中關閉會報錯 “form.resetFields is not a function”。 - 代碼走查顯示,項目內部統一使用
destroyOnHidden(非 antd 原生的 destroyOnClose)控制關閉時銷燬。風險點在於關閉時form實例可能尚未就緒或已被銷燬,從而直接調用resetFields報錯。 - 目標:最小化修復,保證兼容、不引入依賴、通過全部測試。
最小修復點(代碼 diff)
- 在
ModalForm與DrawerForm的關閉重置邏輯裏,為resetFields增加安全存在性檢查,避免在form尚未初始化或已銷燬時調用。
代碼引用:
const resetFields = useCallback(() => {
const form = rest.form ?? rest.formRef?.current ?? formRef.current;
// 重置表單
if (modalProps?.destroyOnHidden && form && typeof (form as any).resetFields === 'function') {
(form as any).resetFields();
}
}, [modalProps?.destroyOnHidden, rest.form, rest.formRef]);
const resetFields = useCallback(() => {
const form = rest.formRef?.current ?? rest.form ?? formRef.current;
// 重置表單
if (drawerProps?.destroyOnHidden && form && typeof (form as any).resetFields === 'function') {
(form as any).resetFields();
}
}, [drawerProps?.destroyOnHidden, rest.form, rest.formRef]);
測試補充
新增用例:在 ModalForm 使用 request 且 destroyOnHidden: true 時,打開後立即關閉(加載中),不應報錯。
代碼引用:
it('📦 ModalForm close during request loading with destroyOnHidden should not throw', async () => {
const wrapper = render(
<ModalForm
width={600}
modalProps={{ destroyOnHidden: true }}
request={async () => {
// Simulate slow request
return new Promise((resolve) => setTimeout(() => resolve({ name: 'demo' }), 200));
}}
trigger={<Button id="new">新建</Button>}
>
<ProFormText name="name" />
</ModalForm>,
);
await act(async () => {
const triggerButton = wrapper.getByText('新 建');
fireEvent.click(triggerButton);
});
// Close before request resolves (cancel button may not render while loading), click close icon
await act(async () => {
const closeButton = document.querySelector('button.ant-modal-close');
if (closeButton) {
fireEvent.click(closeButton);
}
});
// Wait a bit for async to flush
await waitForWaitTime(300);
});
- 本地驗證:已運行完整
pnpm test,全部通過(71 files, 1563 passed, 1 skipped)。新增用例單測也通過。
解決思路對比與選擇
- 方案A:在關閉時調用
resetFields前做方法存在性檢查(已選)。最小改動、兼容性好。 - 方案B:在
BaseForm請求完成後再觸發重置。副作用大,改變行為時機,風險高。 - 方案C:關閉時取消/中斷
request。涉及useFetchData取消邏輯,改動面廣。 - 結論:選擇方案A,最小化、穩妥。
提交信息(Conventional Commits)
fix(form): prevent error when closing ModalForm/DrawerForm during request with destroyOnHidden
- Guard resetFields call to ensure form instance and method exist
- Cover scenario with a new test: close ModalForm while request is loading
- Keep backward compatibility; no breaking changes
Closes #8858
標準化 PR 描述(中英雙語)
-
問題背景與復現步驟:
- 使用
ModalForm,同時配置formRef、request與modalProps={{ destroyOnHidden: true }}。 - 打開彈窗後,接口加載尚未返回時立即關閉。
- 控制枱出現錯誤 “form.resetFields is not a function”。
- 使用
-
解決方案及原因:
- 在關閉時重置表單邏輯中,增加
resetFields方法存在性檢查,避免在form尚未初始化或已被銷燬時調用導致報錯。該方案最小化修改、保持既有行為。
- 在關閉時重置表單邏輯中,增加
-
修改點總結:
src/form/layouts/ModalForm/index.tsx:關閉重置邏輯加方法存在性判斷。src/form/layouts/DrawerForm/index.tsx:同上。- 新增測試
tests/form/modalForm.test.tsx:覆蓋“加載中關閉不報錯”的場景。
-
測試結果:
- 新增用例通過。
- 全量測試通過:71 files,1563 passed,1 skipped。
English:
- Background & Repro:
1) WithModalForm, configureformRef,request, andmodalProps={{ destroyOnHidden: true }}.
2) Open the modal and close it before the asyncrequestresolves.
3) An error is thrown: “form.resetFields is not a function”. -
Solution & Rationale:
- Add a guard before calling
resetFieldsto ensure the form instance exists and the method is available. This avoids calling it during loading/unmount phases. Minimal and non-breaking change.
- Add a guard before calling
-
Changes:
- Add safe guard in
ModalFormandDrawerFormreset logic. - Add a test to cover closing during request loading.
- Add safe guard in
-
Test Results:
- New test passes.
- Full suite passes (71 files, 1563 passed, 1 skipped).
注意點
- 未引入第三方依賴。
- 兼容性良好,不改變 API。
- 與項目
destroyOnHidden語義一致,不混用destroyOnClose。 - 保持原行為:僅在
destroyOnHidden為真時重置表單。