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 1/5] 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 3b3c71159..4a957f4c3 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 08e8c5344..0a5f1a932 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 86be6cdb1..78522bc3c 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 2/5] fix: lint --- src/useWatch.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/useWatch.ts b/src/useWatch.ts index 0a5f1a932..8052e5894 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 250a65a97bea3cd718c732fc9862eddbeafae6a0 Mon Sep 17 00:00:00 2001 From: crazyair <645381995@qq.com> Date: Thu, 11 Sep 2025 20:26:41 +0800 Subject: [PATCH 3/5] feat: useWatch init object --- docs/examples/useWatch-selector.tsx | 5 +++++ src/useWatch.ts | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/examples/useWatch-selector.tsx b/docs/examples/useWatch-selector.tsx index 7ecde6896..656bf1324 100644 --- a/docs/examples/useWatch-selector.tsx +++ b/docs/examples/useWatch-selector.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-shadow */ import React from 'react'; import Form, { Field } from 'rc-field-form'; import Input from './components/Input'; @@ -10,11 +11,15 @@ type FieldType = { export default () => { const [form] = Form.useForm(); + const base = Form.useWatch(values => ({ newName: values.name }), form); + console.log('base', base); + const values = Form.useWatch( values => ({ init: values.init, newName: values.name, newAge: values.age }), { form, preserve: true }, ); console.log('values', values); + return ( <>
diff --git a/src/useWatch.ts b/src/useWatch.ts index 8052e5894..024f173cc 100644 --- a/src/useWatch.ts +++ b/src/useWatch.ts @@ -92,7 +92,9 @@ function useWatch( const options = isFormInstance(_form) ? { form: _form } : _form; const form = options.form; - const [value, setValue] = useState(); + const [value, setValue] = useState( + typeof dependencies === 'function' ? dependencies({}) : undefined, + ); const valueStr = useMemo(() => stringify(value), [value]); const valueStrRef = useRef(valueStr); @@ -117,8 +119,8 @@ function useWatch( // ============================= Update ============================= const triggerUpdate = useEvent((values?: any, allValues?: any) => { const watchValue = options.preserve - ? (allValues ?? getFieldsValue(true)) - : (values ?? getFieldsValue()); + ? allValues ?? getFieldsValue(true) + : values ?? getFieldsValue(); const nextValue = typeof dependencies === 'function' From 182e169aa61e9fb616f7be9908721c1c9b230b0c Mon Sep 17 00:00:00 2001 From: crazyair <645381995@qq.com> Date: Sat, 20 Sep 2025 20:28:43 +0800 Subject: [PATCH 4/5] feat: review --- docs/examples/useWatch-selector.tsx | 4 ++-- src/useWatch.ts | 7 +++++-- tests/useWatch.test.tsx | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/examples/useWatch-selector.tsx b/docs/examples/useWatch-selector.tsx index 656bf1324..037b6bd4d 100644 --- a/docs/examples/useWatch-selector.tsx +++ b/docs/examples/useWatch-selector.tsx @@ -11,8 +11,8 @@ type FieldType = { export default () => { const [form] = Form.useForm(); - const base = Form.useWatch(values => ({ newName: values.name }), form); - console.log('base', base); + const firstEmptyObject = Form.useWatch(values => ({ newName: values.name }), form); + console.log('firstEmptyObject', firstEmptyObject); const values = Form.useWatch( values => ({ init: values.init, newName: values.name, newAge: values.age }), diff --git a/src/useWatch.ts b/src/useWatch.ts index 024f173cc..7528f0d36 100644 --- a/src/useWatch.ts +++ b/src/useWatch.ts @@ -92,10 +92,13 @@ function useWatch( const options = isFormInstance(_form) ? { form: _form } : _form; const form = options.form; - const [value, setValue] = useState( - typeof dependencies === 'function' ? dependencies({}) : undefined, + const dependenciesMemo = useMemo( + () => (typeof dependencies === 'function' ? dependencies({}) : undefined), + [dependencies], ); + const [value, setValue] = useState(dependenciesMemo); + const valueStr = useMemo(() => stringify(value), [value]); const valueStrRef = useRef(valueStr); valueStrRef.current = valueStr; diff --git a/tests/useWatch.test.tsx b/tests/useWatch.test.tsx index 78522bc3c..b26dfcf1c 100644 --- a/tests/useWatch.test.tsx +++ b/tests/useWatch.test.tsx @@ -518,4 +518,22 @@ describe('useWatch', () => { await changeValue(input[0], 'bamboo2'); expect(container.querySelector('.values')?.textContent).toEqual('bamboo2'); }); + it('selector by first no undefined', async () => { + const list: any[] = []; + const Demo = () => { + const [form] = Form.useForm<{ name?: string }>(); + const data = Form.useWatch(values => values, form); + list.push(data); + return ( + + + + + + ); + }; + render(); + expect(list[0]).toEqual({}); + expect(list[1]).toEqual({ name: 'bamboo' }); + }); }); From 1a98550be2e82facb2e973fb17ddf9a5e28c6f19 Mon Sep 17 00:00:00 2001 From: crazyair <645381995@qq.com> Date: Mon, 22 Sep 2025 20:12:18 +0800 Subject: [PATCH 5/5] feat: review --- src/useWatch.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/useWatch.ts b/src/useWatch.ts index 7528f0d36..b3f93e830 100644 --- a/src/useWatch.ts +++ b/src/useWatch.ts @@ -92,13 +92,10 @@ function useWatch( const options = isFormInstance(_form) ? { form: _form } : _form; const form = options.form; - const dependenciesMemo = useMemo( - () => (typeof dependencies === 'function' ? dependencies({}) : undefined), - [dependencies], + const [value, setValue] = useState(() => + typeof dependencies === 'function' ? dependencies({}) : undefined, ); - const [value, setValue] = useState(dependenciesMemo); - const valueStr = useMemo(() => stringify(value), [value]); const valueStrRef = useRef(valueStr); valueStrRef.current = valueStr;