Skip to content

Commit 27328cb

Browse files
committed
feat: useWatch support dynamic names
1 parent 35e81ea commit 27328cb

File tree

3 files changed

+95
-114
lines changed

3 files changed

+95
-114
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: 52 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import type {
77
InternalNamePath,
88
NamePath,
99
Store,
10+
WatchCallBack,
1011
WatchOptions,
1112
} from './interface';
1213
import { isFormInstance } from './utils/typeUtil';
1314
import { getNamePath, getValue } from './utils/valueUtil';
15+
import { useEvent } from '@rc-component/util';
1416

1517
type ReturnPromise<T> = T extends Promise<infer ValueType> ? ValueType : never;
1618
type GetGeneric<TForm extends FormInstance> = ReturnPromise<ReturnType<TForm['validateFields']>>;
@@ -23,19 +25,6 @@ export function stringify(value: any) {
2325
}
2426
}
2527

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-
3928
function useWatch<
4029
TDependencies1 extends keyof GetGeneric<TForm>,
4130
TForm extends FormInstance,
@@ -123,56 +112,57 @@ function useWatch(
123112
);
124113
}
125114

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
115+
// ============================== Form ==============================
116+
const { getFieldsValue, getInternalHooks } = formInstance;
117+
const { registerWatch } = getInternalHooks(HOOK_MARK);
118+
119+
// ============================= Update =============================
120+
const triggerUpdate = useEvent((values?: any, allValues?: any) => {
121+
const watchValue = options.preserve
122+
? (allValues ?? getFieldsValue(true))
123+
: (values ?? getFieldsValue());
124+
125+
const nextValue =
126+
typeof dependencies === 'function'
127+
? dependencies(watchValue)
128+
: getValue(watchValue, getNamePath(dependencies));
129+
130+
if (stringify(value) !== stringify(nextValue)) {
131+
setValue(nextValue);
132+
}
133+
});
134+
135+
// ============================= Effect =============================
136+
const flattenDeps =
137+
typeof dependencies === 'function' ? dependencies : JSON.stringify(dependencies);
138+
139+
// Deps changed
140+
useEffect(() => {
141+
// Skip if not exist form instance
142+
if (!isValidForm) {
143+
return;
144+
}
145+
146+
triggerUpdate();
147+
148+
// eslint-disable-next-line react-hooks/exhaustive-deps
149+
}, [isValidForm, flattenDeps]);
150+
151+
// Value changed
152+
useEffect(() => {
153+
// Skip if not exist form instance
154+
if (!isValidForm) {
155+
return;
156+
}
157+
158+
const cancelRegister = registerWatch((values, allValues) => {
159+
triggerUpdate(values, allValues);
160+
});
161+
162+
return cancelRegister;
163+
173164
// eslint-disable-next-line react-hooks/exhaustive-deps
174-
[isValidForm],
175-
);
165+
}, [isValidForm]);
176166

177167
return value;
178168
}

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)