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..8052e589 100644 --- a/src/useWatch.ts +++ b/src/useWatch.ts @@ -4,13 +4,13 @@ import FieldContext, { HOOK_MARK } from './FieldContext'; import type { FormInstance, InternalFormInstance, - InternalNamePath, NamePath, Store, 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 +23,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 +110,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 () => {