Skip to content

Commit 84bff2e

Browse files
authored
feat: useWatch support dynamic names (#758)
* feat: useWatch support dynamic names * fix: lint
1 parent 35e81ea commit 84bff2e

File tree

3 files changed

+94
-115
lines changed

3 files changed

+94
-115
lines changed

docs/examples/debug.tsx

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,30 @@
11
import Form, { Field } from 'rc-field-form';
22
import React from 'react';
3-
4-
function WatchCom({ name }: { name: number }) {
5-
const data = Form.useWatch(name);
6-
return data;
7-
}
3+
import Input from './components/Input';
84

95
export default function App() {
106
const [form] = Form.useForm();
11-
const [open, setOpen] = React.useState<boolean>(true);
7+
const [keyName, setKeyName] = React.useState(true);
128

13-
const data = React.useMemo(() => {
14-
return Array.from({ length: 1 * 500 }).map((_, i) => ({
15-
key: i,
16-
name: `Edward King ${i}`,
17-
age: 32,
18-
address: `London, Park Lane no. ${i}`,
19-
}));
20-
}, []);
9+
// const val = Form.useWatch(keyName ? 'name' : 'age', form);
10+
const val = Form.useWatch(values => values[keyName ? 'name' : 'age'], form);
2111

2212
return (
2313
<Form form={form}>
24-
<div className="App">
25-
{/* When I made the switch, it was very laggy */}
26-
<button
27-
onClick={() => {
28-
setOpen(!open);
29-
}}
30-
>
31-
Switch
32-
</button>
33-
<WatchCom name={0} />
34-
<WatchCom name={1} />
35-
{open ? (
36-
<div className="flex gap-[5px] flex-wrap">
37-
{data?.map(item => {
38-
return (
39-
<Field key={item.name} name={item.name}>
40-
<input />
41-
</Field>
42-
);
43-
})}
44-
</div>
45-
) : (
46-
<h2>some thing</h2>
47-
)}
48-
</div>
14+
<button
15+
onClick={() => {
16+
setKeyName(!keyName);
17+
}}
18+
>
19+
Switch {String(keyName)}
20+
</button>
21+
<Field name="name" initialValue="bamboo">
22+
<Input />
23+
</Field>
24+
<Field name="age" initialValue="light">
25+
<Input />
26+
</Field>
27+
{val}
4928
</Form>
5029
);
5130
}

src/useWatch.ts

Lines changed: 51 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import FieldContext, { HOOK_MARK } from './FieldContext';
44
import type {
55
FormInstance,
66
InternalFormInstance,
7-
InternalNamePath,
87
NamePath,
98
Store,
109
WatchOptions,
1110
} from './interface';
1211
import { isFormInstance } from './utils/typeUtil';
1312
import { getNamePath, getValue } from './utils/valueUtil';
13+
import { useEvent } from '@rc-component/util';
1414

1515
type ReturnPromise<T> = T extends Promise<infer ValueType> ? ValueType : never;
1616
type GetGeneric<TForm extends FormInstance> = ReturnPromise<ReturnType<TForm['validateFields']>>;
@@ -23,19 +23,6 @@ export function stringify(value: any) {
2323
}
2424
}
2525

26-
const useWatchWarning =
27-
process.env.NODE_ENV !== 'production'
28-
? (namePath: InternalNamePath) => {
29-
const fullyStr = namePath.join('__RC_FIELD_FORM_SPLIT__');
30-
const nameStrRef = useRef(fullyStr);
31-
32-
warning(
33-
nameStrRef.current === fullyStr,
34-
'`useWatch` is not support dynamic `namePath`. Please provide static instead.',
35-
);
36-
}
37-
: () => {};
38-
3926
function useWatch<
4027
TDependencies1 extends keyof GetGeneric<TForm>,
4128
TForm extends FormInstance,
@@ -123,56 +110,57 @@ function useWatch(
123110
);
124111
}
125112

126-
const namePath = getNamePath(dependencies);
127-
const namePathRef = useRef(namePath);
128-
namePathRef.current = namePath;
129-
130-
useWatchWarning(namePath);
131-
132-
useEffect(
133-
() => {
134-
// Skip if not exist form instance
135-
if (!isValidForm) {
136-
return;
137-
}
138-
139-
const { getFieldsValue, getInternalHooks } = formInstance;
140-
const { registerWatch } = getInternalHooks(HOOK_MARK);
141-
142-
const getWatchValue = (values: any, allValues: any) => {
143-
const watchValue = options.preserve ? allValues : values;
144-
return typeof dependencies === 'function'
145-
? dependencies(watchValue)
146-
: getValue(watchValue, namePathRef.current);
147-
};
148-
149-
const cancelRegister = registerWatch((values, allValues) => {
150-
const newValue = getWatchValue(values, allValues);
151-
const nextValueStr = stringify(newValue);
152-
153-
// Compare stringify in case it's nest object
154-
if (valueStrRef.current !== nextValueStr) {
155-
valueStrRef.current = nextValueStr;
156-
setValue(newValue);
157-
}
158-
});
159-
160-
// TODO: We can improve this perf in future
161-
const initialValue = getWatchValue(getFieldsValue(), getFieldsValue(true));
162-
163-
// React 18 has the bug that will queue update twice even the value is not changed
164-
// ref: https://github.com/facebook/react/issues/27213
165-
if (value !== initialValue) {
166-
setValue(initialValue);
167-
}
168-
169-
return cancelRegister;
170-
},
171-
172-
// We do not need re-register since namePath content is the same
113+
// ============================== Form ==============================
114+
const { getFieldsValue, getInternalHooks } = formInstance;
115+
const { registerWatch } = getInternalHooks(HOOK_MARK);
116+
117+
// ============================= Update =============================
118+
const triggerUpdate = useEvent((values?: any, allValues?: any) => {
119+
const watchValue = options.preserve
120+
? (allValues ?? getFieldsValue(true))
121+
: (values ?? getFieldsValue());
122+
123+
const nextValue =
124+
typeof dependencies === 'function'
125+
? dependencies(watchValue)
126+
: getValue(watchValue, getNamePath(dependencies));
127+
128+
if (stringify(value) !== stringify(nextValue)) {
129+
setValue(nextValue);
130+
}
131+
});
132+
133+
// ============================= Effect =============================
134+
const flattenDeps =
135+
typeof dependencies === 'function' ? dependencies : JSON.stringify(dependencies);
136+
137+
// Deps changed
138+
useEffect(() => {
139+
// Skip if not exist form instance
140+
if (!isValidForm) {
141+
return;
142+
}
143+
144+
triggerUpdate();
145+
146+
// eslint-disable-next-line react-hooks/exhaustive-deps
147+
}, [isValidForm, flattenDeps]);
148+
149+
// Value changed
150+
useEffect(() => {
151+
// Skip if not exist form instance
152+
if (!isValidForm) {
153+
return;
154+
}
155+
156+
const cancelRegister = registerWatch((values, allValues) => {
157+
triggerUpdate(values, allValues);
158+
});
159+
160+
return cancelRegister;
161+
173162
// eslint-disable-next-line react-hooks/exhaustive-deps
174-
[isValidForm],
175-
);
163+
}, [isValidForm]);
176164

177165
return value;
178166
}

tests/useWatch.test.tsx

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -426,25 +426,37 @@ describe('useWatch', () => {
426426
errorSpy.mockRestore();
427427
});
428428

429-
it('dynamic change warning', () => {
430-
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
429+
it('dynamic change', () => {
431430
const Demo: React.FC = () => {
432431
const [form] = Form.useForm();
433432
const [watchPath, setWatchPath] = React.useState('light');
434-
Form.useWatch(watchPath, form);
435-
436-
React.useEffect(() => {
437-
setWatchPath('bamboo');
438-
}, []);
433+
const value = Form.useWatch(watchPath, form);
439434

440-
return <Form form={form} />;
435+
return (
436+
<Form form={form} initialValues={{ light: 1128, bamboo: 903 }}>
437+
<Field name="light">
438+
<Input />
439+
</Field>
440+
<Field name="bamboo">
441+
<Input />
442+
</Field>
443+
<button
444+
onClick={() => {
445+
setWatchPath('bamboo');
446+
}}
447+
>
448+
{value}
449+
</button>
450+
</Form>
451+
);
441452
};
442-
render(<Demo />);
443453

444-
expect(errorSpy).toHaveBeenCalledWith(
445-
'Warning: `useWatch` is not support dynamic `namePath`. Please provide static instead.',
446-
);
447-
errorSpy.mockRestore();
454+
const { container } = render(<Demo />);
455+
const btn = container.querySelector('button')!;
456+
expect(btn.textContent).toEqual('1128');
457+
458+
fireEvent.click(btn);
459+
expect(btn.textContent).toEqual('903');
448460
});
449461

450462
it('useWatch with preserve option', async () => {

0 commit comments

Comments
 (0)