From 27328cb910d01e15bd526d2a22aff8c1d2b9c407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 1 Jul 2025 16:03:28 +0800 Subject: [PATCH 01/12] feat: useWatch support dynamic names --- docs/examples/debug.tsx | 57 +++++++------------- src/useWatch.ts | 114 ++++++++++++++++++---------------------- tests/useWatch.test.tsx | 38 +++++++++----- 3 files changed, 95 insertions(+), 114 deletions(-) diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index 3b3c7115..4a957f4c 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -1,51 +1,30 @@ import Form, { Field } from 'rc-field-form'; import React from 'react'; - -function WatchCom({ name }: { name: number }) { - const data = Form.useWatch(name); - return data; -} +import Input from './components/Input'; export default function App() { const [form] = Form.useForm(); - const [open, setOpen] = React.useState(true); + const [keyName, setKeyName] = React.useState(true); - const data = React.useMemo(() => { - return Array.from({ length: 1 * 500 }).map((_, i) => ({ - key: i, - name: `Edward King ${i}`, - age: 32, - address: `London, Park Lane no. ${i}`, - })); - }, []); + // const val = Form.useWatch(keyName ? 'name' : 'age', form); + const val = Form.useWatch(values => values[keyName ? 'name' : 'age'], form); return (
-
- {/* When I made the switch, it was very laggy */} - - - - {open ? ( -
- {data?.map(item => { - return ( - - - - ); - })} -
- ) : ( -

some thing

- )} -
+ + + + + + + + {val}
); } diff --git a/src/useWatch.ts b/src/useWatch.ts index 08e8c534..0a5f1a93 100644 --- a/src/useWatch.ts +++ b/src/useWatch.ts @@ -7,10 +7,12 @@ import type { InternalNamePath, NamePath, Store, + WatchCallBack, WatchOptions, } from './interface'; import { isFormInstance } from './utils/typeUtil'; import { getNamePath, getValue } from './utils/valueUtil'; +import { useEvent } from '@rc-component/util'; type ReturnPromise = T extends Promise ? ValueType : never; type GetGeneric = ReturnPromise>; @@ -23,19 +25,6 @@ export function stringify(value: any) { } } -const useWatchWarning = - process.env.NODE_ENV !== 'production' - ? (namePath: InternalNamePath) => { - const fullyStr = namePath.join('__RC_FIELD_FORM_SPLIT__'); - const nameStrRef = useRef(fullyStr); - - warning( - nameStrRef.current === fullyStr, - '`useWatch` is not support dynamic `namePath`. Please provide static instead.', - ); - } - : () => {}; - function useWatch< TDependencies1 extends keyof GetGeneric, TForm extends FormInstance, @@ -123,56 +112,57 @@ function useWatch( ); } - const namePath = getNamePath(dependencies); - const namePathRef = useRef(namePath); - namePathRef.current = namePath; - - useWatchWarning(namePath); - - useEffect( - () => { - // Skip if not exist form instance - if (!isValidForm) { - return; - } - - const { getFieldsValue, getInternalHooks } = formInstance; - const { registerWatch } = getInternalHooks(HOOK_MARK); - - const getWatchValue = (values: any, allValues: any) => { - const watchValue = options.preserve ? allValues : values; - return typeof dependencies === 'function' - ? dependencies(watchValue) - : getValue(watchValue, namePathRef.current); - }; - - const cancelRegister = registerWatch((values, allValues) => { - const newValue = getWatchValue(values, allValues); - const nextValueStr = stringify(newValue); - - // Compare stringify in case it's nest object - if (valueStrRef.current !== nextValueStr) { - valueStrRef.current = nextValueStr; - setValue(newValue); - } - }); - - // TODO: We can improve this perf in future - const initialValue = getWatchValue(getFieldsValue(), getFieldsValue(true)); - - // React 18 has the bug that will queue update twice even the value is not changed - // ref: https://github.com/facebook/react/issues/27213 - if (value !== initialValue) { - setValue(initialValue); - } - - return cancelRegister; - }, - - // We do not need re-register since namePath content is the same + // ============================== Form ============================== + const { getFieldsValue, getInternalHooks } = formInstance; + const { registerWatch } = getInternalHooks(HOOK_MARK); + + // ============================= Update ============================= + const triggerUpdate = useEvent((values?: any, allValues?: any) => { + const watchValue = options.preserve + ? (allValues ?? getFieldsValue(true)) + : (values ?? getFieldsValue()); + + const nextValue = + typeof dependencies === 'function' + ? dependencies(watchValue) + : getValue(watchValue, getNamePath(dependencies)); + + if (stringify(value) !== stringify(nextValue)) { + setValue(nextValue); + } + }); + + // ============================= Effect ============================= + const flattenDeps = + typeof dependencies === 'function' ? dependencies : JSON.stringify(dependencies); + + // Deps changed + useEffect(() => { + // Skip if not exist form instance + if (!isValidForm) { + return; + } + + triggerUpdate(); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isValidForm, flattenDeps]); + + // Value changed + useEffect(() => { + // Skip if not exist form instance + if (!isValidForm) { + return; + } + + const cancelRegister = registerWatch((values, allValues) => { + triggerUpdate(values, allValues); + }); + + return cancelRegister; + // eslint-disable-next-line react-hooks/exhaustive-deps - [isValidForm], - ); + }, [isValidForm]); return value; } diff --git a/tests/useWatch.test.tsx b/tests/useWatch.test.tsx index 86be6cdb..78522bc3 100644 --- a/tests/useWatch.test.tsx +++ b/tests/useWatch.test.tsx @@ -426,25 +426,37 @@ describe('useWatch', () => { errorSpy.mockRestore(); }); - it('dynamic change warning', () => { - const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + it('dynamic change', () => { const Demo: React.FC = () => { const [form] = Form.useForm(); const [watchPath, setWatchPath] = React.useState('light'); - Form.useWatch(watchPath, form); - - React.useEffect(() => { - setWatchPath('bamboo'); - }, []); + const value = Form.useWatch(watchPath, form); - return
; + return ( + + + + + + + + +
+ ); }; - render(); - expect(errorSpy).toHaveBeenCalledWith( - 'Warning: `useWatch` is not support dynamic `namePath`. Please provide static instead.', - ); - errorSpy.mockRestore(); + const { container } = render(); + const btn = container.querySelector('button')!; + expect(btn.textContent).toEqual('1128'); + + fireEvent.click(btn); + expect(btn.textContent).toEqual('903'); }); it('useWatch with preserve option', async () => { From 1c2a4691c55e333855956240af15a2d6028f401f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 1 Jul 2025 16:18:55 +0800 Subject: [PATCH 02/12] fix: lint --- src/useWatch.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/useWatch.ts b/src/useWatch.ts index 0a5f1a93..8052e589 100644 --- a/src/useWatch.ts +++ b/src/useWatch.ts @@ -4,10 +4,8 @@ import FieldContext, { HOOK_MARK } from './FieldContext'; import type { FormInstance, InternalFormInstance, - InternalNamePath, NamePath, Store, - WatchCallBack, WatchOptions, } from './interface'; import { isFormInstance } from './utils/typeUtil'; From cec82b3bf1a169d2240f145c55a9cf2dbbb1f48e Mon Sep 17 00:00:00 2001 From: crazyair <645381995@qq.com> Date: Wed, 3 Sep 2025 21:11:40 +0800 Subject: [PATCH 03/12] feat: test --- docs/demo/list-unmount.md | 3 ++ docs/examples/list-unmount.tsx | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 docs/demo/list-unmount.md create mode 100644 docs/examples/list-unmount.tsx diff --git a/docs/demo/list-unmount.md b/docs/demo/list-unmount.md new file mode 100644 index 00000000..b7154eec --- /dev/null +++ b/docs/demo/list-unmount.md @@ -0,0 +1,3 @@ +## list + + diff --git a/docs/examples/list-unmount.tsx b/docs/examples/list-unmount.tsx new file mode 100644 index 00000000..b62f9586 --- /dev/null +++ b/docs/examples/list-unmount.tsx @@ -0,0 +1,56 @@ +import React, { useState } from 'react'; +import Form from 'rc-field-form'; +import Input from './components/Input'; +import LabelField from './components/LabelField'; + +const Demo = () => { + const [form] = Form.useForm(); + const [isShow, setIsShow] = useState(true); + + return ( +
+
{ + console.log(JSON.stringify(values, null, 2)); + console.log(JSON.stringify(form.getFieldsValue({ strict: true }), null, 2)); + }} + initialValues={{ + users: [ + { name: 'a', age: '1' }, + { name: 'b', age: '2' }, + ], + }} + > + {() => JSON.stringify(form.getFieldsValue(), null, 2)} + + + {fields => { + return ( +
+ {fields.map(field => ( +
+ + + + {isShow && ( + + + + )} +
+ ))} +
+ ); + }} +
+ + +
+
+ ); +}; + +export default Demo; From e37a00201cfcdb0996d6c1ad6598dd7864b186e9 Mon Sep 17 00:00:00 2001 From: crazyair <645381995@qq.com> Date: Wed, 3 Sep 2025 21:20:59 +0800 Subject: [PATCH 04/12] feat: test --- src/useForm.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/useForm.ts b/src/useForm.ts index d948c074..c534f911 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -921,6 +921,9 @@ export class FormStore { const { recursive, dirty } = options || {}; this.getFieldEntities(true).forEach((field: FieldEntity) => { + if (field.isList()) { + return; + } // Add field if not provide `nameList` if (!provideNameList) { namePathList.push(field.getNamePath()); From 719b59bc21d10580f8c11c279e665d5b8d1b3ee3 Mon Sep 17 00:00:00 2001 From: crazyair <645381995@qq.com> Date: Thu, 4 Sep 2025 12:40:42 +0800 Subject: [PATCH 05/12] feat: use strict --- src/useForm.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/useForm.ts b/src/useForm.ts index c534f911..cb0c9d06 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -921,9 +921,6 @@ export class FormStore { const { recursive, dirty } = options || {}; this.getFieldEntities(true).forEach((field: FieldEntity) => { - if (field.isList()) { - return; - } // Add field if not provide `nameList` if (!provideNameList) { namePathList.push(field.getNamePath()); @@ -1003,7 +1000,7 @@ export class FormStore { const returnPromise: Promise = summaryPromise .then((): Promise => { if (this.lastValidatePromise === summaryPromise) { - return Promise.resolve(this.getFieldsValue(namePathList)); + return Promise.resolve(this.getFieldsValue({ strict: true })); } return Promise.reject([]); }) From 6f2f069ed8132918ee07952f65e1f7812ed5330f Mon Sep 17 00:00:00 2001 From: crazyair <645381995@qq.com> Date: Fri, 12 Sep 2025 09:47:56 +0800 Subject: [PATCH 06/12] feat: test --- src/useForm.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/useForm.ts b/src/useForm.ts index cb0c9d06..ed3ad023 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -921,9 +921,16 @@ export class FormStore { const { recursive, dirty } = options || {}; this.getFieldEntities(true).forEach((field: FieldEntity) => { + const fieldNamePath = field.getNamePath(); + // Add field if not provide `nameList` if (!provideNameList) { - namePathList.push(field.getNamePath()); + if (field.isList()) { + if (namePathList.find(name => name.toString().includes(fieldNamePath.toString()))) { + return; + } + } + namePathList.push(fieldNamePath); } // Skip if without rule @@ -936,7 +943,6 @@ export class FormStore { return; } - const fieldNamePath = field.getNamePath(); validateNamePathList.add(fieldNamePath.join(TMP_SPLIT)); // Add field validate rule in to promise list @@ -1000,7 +1006,7 @@ export class FormStore { const returnPromise: Promise = summaryPromise .then((): Promise => { if (this.lastValidatePromise === summaryPromise) { - return Promise.resolve(this.getFieldsValue({ strict: true })); + return Promise.resolve(this.getFieldsValue(namePathList)); } return Promise.reject([]); }) From a43a38a2bdc94471c690188eab68647aad802fbc Mon Sep 17 00:00:00 2001 From: crazyair <645381995@qq.com> Date: Sat, 20 Sep 2025 20:15:36 +0800 Subject: [PATCH 07/12] feat: review --- src/useForm.ts | 6 ++--- tests/list.test.tsx | 65 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/useForm.ts b/src/useForm.ts index ed3ad023..9e638912 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -925,10 +925,8 @@ export class FormStore { // Add field if not provide `nameList` if (!provideNameList) { - if (field.isList()) { - if (namePathList.find(name => name.toString().includes(fieldNamePath.toString()))) { - return; - } + if (field.isList() && namePathList.some(name => matchNamePath(name, fieldNamePath, true))) { + return; } namePathList.push(fieldNamePath); } diff --git a/tests/list.test.tsx b/tests/list.test.tsx index 598eb5f5..ace99c2d 100644 --- a/tests/list.test.tsx +++ b/tests/list.test.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { fireEvent, render, act } from '@testing-library/react'; import { resetWarned } from '@rc-component/util/lib/warning'; import Form, { Field, List } from '../src'; @@ -937,4 +937,67 @@ describe('Form.List', () => { expect(formRef.current!.getFieldValue('list')).toEqual([{ user: '1' }, { user: '3' }]); }); + + it('list unmount', async () => { + const valueRef = React.createRef(); + + const Demo = () => { + const [isShow, setIsShow] = useState(true); + return ( +
{ + valueRef.current = values; + }} + > + + {fields => { + return fields.map(field => ( +
+ + + + {isShow && ( + + + + )} +
+ )); + }} +
+ + +
+ ); + }; + + const { queryByTestId } = render(); + fireEvent.click(queryByTestId('submit')); + await act(async () => { + await timeout(); + }); + expect(valueRef.current).toEqual({ + users: [ + { name: 'a', age: '1' }, + { name: 'b', age: '2' }, + ], + }); + + fireEvent.click(queryByTestId('hide')); + fireEvent.click(queryByTestId('submit')); + await act(async () => { + await timeout(); + }); + expect(valueRef.current).toEqual({ users: [{ name: 'a' }, { name: 'b' }] }); + }); }); From 7ffbcb0d4b6b04fea7802eb15db476c53fc895bf Mon Sep 17 00:00:00 2001 From: crazyair <645381995@qq.com> Date: Mon, 22 Sep 2025 20:25:47 +0800 Subject: [PATCH 08/12] feat: review --- src/useForm.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/useForm.ts b/src/useForm.ts index 9e638912..81978bb8 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -911,6 +911,8 @@ export class FormStore { ? nameList.map(getNamePath) : []; + const removeListNameStrList: string[] = []; + // Collect result in promise list const promiseList: Promise[] = []; @@ -926,7 +928,7 @@ export class FormStore { // Add field if not provide `nameList` if (!provideNameList) { if (field.isList() && namePathList.some(name => matchNamePath(name, fieldNamePath, true))) { - return; + removeListNameStrList.push(fieldNamePath.toString()); } namePathList.push(fieldNamePath); } @@ -1004,7 +1006,10 @@ export class FormStore { const returnPromise: Promise = summaryPromise .then((): Promise => { if (this.lastValidatePromise === summaryPromise) { - return Promise.resolve(this.getFieldsValue(namePathList)); + const filterListNameList = namePathList.filter( + name => !removeListNameStrList.some(nameStr => nameStr === name.toString()), + ); + return Promise.resolve(this.getFieldsValue(filterListNameList)); } return Promise.reject([]); }) From 3282cbaeba0abf4a6771f4192b44c9c975cb225f Mon Sep 17 00:00:00 2001 From: crazyair <645381995@qq.com> Date: Mon, 22 Sep 2025 20:29:29 +0800 Subject: [PATCH 09/12] feat: add test --- tests/list.test.tsx | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/list.test.tsx b/tests/list.test.tsx index ace99c2d..e21a8f84 100644 --- a/tests/list.test.tsx +++ b/tests/list.test.tsx @@ -1000,4 +1000,35 @@ describe('Form.List', () => { }); expect(valueRef.current).toEqual({ users: [{ name: 'a' }, { name: 'b' }] }); }); + + it('list rules', async () => { + const onFinishFailed = jest.fn(); + + const Demo = () => { + return ( +
+ Promise.reject('error') }]}> + {fields => { + return fields.map(field => ( + + + + )); + }} + + +
+ ); + }; + + const { queryByTestId } = render(); + fireEvent.click(queryByTestId('submit')); + await act(async () => { + await timeout(); + }); + + expect(onFinishFailed).toHaveBeenCalled(); + }); }); From 4e69d82ce708e6ccbdd8ec6d9fbc4fa8241df7b5 Mon Sep 17 00:00:00 2001 From: crazyair <645381995@qq.com> Date: Sun, 28 Sep 2025 09:59:30 +0800 Subject: [PATCH 10/12] feat: review --- src/useForm.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/useForm.ts b/src/useForm.ts index 81978bb8..7ae2a4cb 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -910,8 +910,7 @@ export class FormStore { const namePathList: InternalNamePath[] | undefined = provideNameList ? nameList.map(getNamePath) : []; - - const removeListNameStrList: string[] = []; + const noListNamePathList = [...namePathList]; // Collect result in promise list const promiseList: Promise[] = []; @@ -927,8 +926,11 @@ export class FormStore { // Add field if not provide `nameList` if (!provideNameList) { - if (field.isList() && namePathList.some(name => matchNamePath(name, fieldNamePath, true))) { - removeListNameStrList.push(fieldNamePath.toString()); + if ( + // When Form.List has a value, filter Form.List `name` + !(field.isList() && namePathList.some(name => matchNamePath(name, fieldNamePath, true))) + ) { + noListNamePathList.push(fieldNamePath); } namePathList.push(fieldNamePath); } @@ -1006,10 +1008,7 @@ export class FormStore { const returnPromise: Promise = summaryPromise .then((): Promise => { if (this.lastValidatePromise === summaryPromise) { - const filterListNameList = namePathList.filter( - name => !removeListNameStrList.some(nameStr => nameStr === name.toString()), - ); - return Promise.resolve(this.getFieldsValue(filterListNameList)); + return Promise.resolve(this.getFieldsValue(noListNamePathList)); } return Promise.reject([]); }) From c7cdfb9eb0684aa632180da5f9ac9f8ebc6e53ec Mon Sep 17 00:00:00 2001 From: crazyair <645381995@qq.com> Date: Mon, 29 Sep 2025 08:36:32 +0800 Subject: [PATCH 11/12] feat: review --- src/useForm.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/useForm.ts b/src/useForm.ts index 7ae2a4cb..38093f06 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -910,7 +910,8 @@ export class FormStore { const namePathList: InternalNamePath[] | undefined = provideNameList ? nameList.map(getNamePath) : []; - const noListNamePathList = [...namePathList]; + + const finalValueNamePathList = [...namePathList]; // Collect result in promise list const promiseList: Promise[] = []; @@ -930,7 +931,7 @@ export class FormStore { // When Form.List has a value, filter Form.List `name` !(field.isList() && namePathList.some(name => matchNamePath(name, fieldNamePath, true))) ) { - noListNamePathList.push(fieldNamePath); + finalValueNamePathList.push(fieldNamePath); } namePathList.push(fieldNamePath); } @@ -1008,7 +1009,7 @@ export class FormStore { const returnPromise: Promise = summaryPromise .then((): Promise => { if (this.lastValidatePromise === summaryPromise) { - return Promise.resolve(this.getFieldsValue(noListNamePathList)); + return Promise.resolve(this.getFieldsValue(finalValueNamePathList)); } return Promise.reject([]); }) From 8c884da01ec48b7377b50a9cc736803dec91b08f Mon Sep 17 00:00:00 2001 From: crazyair <645381995@qq.com> Date: Mon, 29 Sep 2025 08:38:07 +0800 Subject: [PATCH 12/12] feat: review --- src/useForm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/useForm.ts b/src/useForm.ts index 38093f06..7135aead 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -910,7 +910,7 @@ export class FormStore { const namePathList: InternalNamePath[] | undefined = provideNameList ? nameList.map(getNamePath) : []; - + // Same namePathList, but does not include Form.List name const finalValueNamePathList = [...namePathList]; // Collect result in promise list