Skip to content

Commit bde0583

Browse files
QDyanbingzombieJ
andauthored
fix(form): prevent useWatch from triggering twice during Form.List up… (#772)
* fix(form): prevent useWatch from triggering twice during Form.List updates * refactor: move hooks to dedicated directory and update imports [AI] * chore: batcher * chore: of it * chore: update config * test: fix test case * test: simplify * chore: clean up * chore: clean up * chore: fix lint * chore: fix lint --------- Co-authored-by: 二货机器人 <[email protected]>
1 parent 3247c62 commit bde0583

File tree

17 files changed

+274
-212
lines changed

17 files changed

+274
-212
lines changed

docs/examples/debug.tsx

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,53 @@ import Input from './components/Input';
44

55
export default function App() {
66
const [form] = Form.useForm();
7-
const [keyName, setKeyName] = React.useState(true);
7+
const names = Form.useWatch('names', form);
88

9-
// const val = Form.useWatch(keyName ? 'name' : 'age', form);
10-
const val = Form.useWatch(values => values[keyName ? 'name' : 'age'], form);
9+
console.log('[Antd V6] names:', names);
1110

1211
return (
13-
<Form form={form}>
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}
28-
</Form>
12+
<div
13+
style={{
14+
padding: 24,
15+
border: '2px solid #1890ff',
16+
borderRadius: 8,
17+
marginBottom: 24,
18+
}}
19+
>
20+
<h2 style={{ color: '#1890ff' }}>Antd V6 - useWatch + Form.List</h2>
21+
22+
<Form form={form} style={{ maxWidth: 600 }} initialValues={{
23+
names: [
24+
'aaa',
25+
'bbb'
26+
]
27+
}}>
28+
<Form.List name="names">
29+
{(fields, { add, remove }) => {
30+
return (
31+
<>
32+
{fields.map(({key, ...field}, index) => (
33+
<div key={key}>
34+
<Field {...field}>
35+
<Input placeholder="用户名" style={{ width: 200 }} />
36+
</Field>
37+
<button type="button" onClick={() => remove(index)}>
38+
删除
39+
</button>
40+
</div>
41+
))}
42+
43+
<div>
44+
<button type="button" onClick={() => add()}>
45+
+ 添加用户
46+
</button>
47+
<button onClick={() => remove(1)}>删除索引 1</button>
48+
</div>
49+
</>
50+
);
51+
}}
52+
</Form.List>
53+
</Form>
54+
</div>
2955
);
3056
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"gh-pages": "^6.1.0",
7373
"jest": "^29.0.0",
7474
"prettier": "^3.1.0",
75-
"rc-test": "^7.0.15",
75+
"rc-test": "^7.1.3",
7676
"react": "^18.0.0",
7777
"react-dnd": "^8.0.3",
7878
"react-dnd-html5-backend": "^8.0.3",

src/BatchUpdate.tsx

Lines changed: 0 additions & 34 deletions
This file was deleted.

src/FieldContext.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ const Context = React.createContext<InternalFormInstance>({
4242
setValidateMessages: warningFunc,
4343
setPreserve: warningFunc,
4444
getInitialValue: warningFunc,
45-
setBatchUpdate: warningFunc,
4645
};
4746
},
4847
});

src/Form.tsx

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,12 @@ import type {
88
InternalFormInstance,
99
FormRef,
1010
} from './interface';
11-
import useForm from './useForm';
11+
import useForm from './hooks/useForm';
1212
import FieldContext, { HOOK_MARK } from './FieldContext';
1313
import type { FormContextProps } from './FormContext';
1414
import FormContext from './FormContext';
1515
import { isSimilar } from './utils/valueUtil';
1616
import ListContext from './ListContext';
17-
import type { BatchTask, BatchUpdateRef } from './BatchUpdate';
18-
import BatchUpdate from './BatchUpdate';
1917

2018
type BaseFormProps = Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onSubmit' | 'children'>;
2119

@@ -72,7 +70,6 @@ const Form: React.ForwardRefRenderFunction<FormRef, FormProps> = (
7270
setValidateMessages,
7371
setPreserve,
7472
destroyForm,
75-
setBatchUpdate,
7673
} = (formInstance as InternalFormInstance).getInternalHooks(HOOK_MARK);
7774

7875
// Pass ref with form instance
@@ -121,41 +118,6 @@ const Form: React.ForwardRefRenderFunction<FormRef, FormProps> = (
121118
mountRef.current = true;
122119
}
123120

124-
// ======================== Batch Update ========================
125-
// zombieJ:
126-
// To avoid Form self re-render,
127-
// We create a sub component `BatchUpdate` to handle batch update logic.
128-
// When the call with do not change immediate, we will batch the update
129-
// and flush it in `useLayoutEffect` for next tick.
130-
131-
// Set batch update ref
132-
const batchUpdateRef = React.useRef<BatchUpdateRef>(null);
133-
const batchUpdateTasksRef = React.useRef<[key: string, fn: VoidFunction][]>([]);
134-
135-
const tryFlushBatch = () => {
136-
if (batchUpdateRef.current) {
137-
batchUpdateTasksRef.current.forEach(([key, fn]) => {
138-
batchUpdateRef.current.batch(key, fn);
139-
});
140-
batchUpdateTasksRef.current = [];
141-
}
142-
};
143-
144-
// Ref update
145-
const setBatchUpdateRef = React.useCallback((batchUpdate: BatchUpdateRef | null) => {
146-
batchUpdateRef.current = batchUpdate;
147-
tryFlushBatch();
148-
}, []);
149-
150-
// Task list
151-
152-
const batchUpdate: BatchTask = (key, callback) => {
153-
batchUpdateTasksRef.current.push([key, callback]);
154-
tryFlushBatch();
155-
};
156-
157-
setBatchUpdate(batchUpdate);
158-
159121
// ========================== Unmount ===========================
160122
React.useEffect(
161123
() => () => destroyForm(clearOnDestroy),
@@ -197,7 +159,6 @@ const Form: React.ForwardRefRenderFunction<FormRef, FormProps> = (
197159
const wrapperNode = (
198160
<ListContext.Provider value={null}>
199161
<FieldContext.Provider value={formContextValue}>{childrenNode}</FieldContext.Provider>
200-
<BatchUpdate ref={setBatchUpdateRef} />
201162
</ListContext.Provider>
202163
);
203164

src/useForm.ts renamed to src/hooks/useForm.ts

Lines changed: 14 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { merge } from '@rc-component/util/lib/utils/set';
22
import { mergeWith } from '@rc-component/util';
33
import warning from '@rc-component/util/lib/warning';
44
import * as React from 'react';
5-
import { HOOK_MARK } from './FieldContext';
5+
import { HOOK_MARK } from '../FieldContext';
66
import type {
77
Callbacks,
88
FieldData,
@@ -26,20 +26,19 @@ import type {
2626
ValidateErrorEntity,
2727
ValidateMessages,
2828
ValuedNotifyInfo,
29-
WatchCallBack,
30-
} from './interface';
31-
import { allPromiseFinish } from './utils/asyncUtil';
32-
import { defaultValidateMessages } from './utils/messages';
33-
import NameMap from './utils/NameMap';
29+
} from '../interface';
30+
import { allPromiseFinish } from '../utils/asyncUtil';
31+
import { defaultValidateMessages } from '../utils/messages';
32+
import NameMap from '../utils/NameMap';
3433
import {
3534
cloneByNamePathList,
3635
containsNamePath,
3736
getNamePath,
3837
getValue,
3938
matchNamePath,
4039
setValue,
41-
} from './utils/valueUtil';
42-
import type { BatchTask } from './BatchUpdate';
40+
} from '../utils/valueUtil';
41+
import WatcherCenter from './useNotifyWatch';
4342

4443
type FlexibleFieldEntity = Partial<FieldEntity>;
4544

@@ -78,6 +77,8 @@ export class FormStore {
7877

7978
private lastValidatePromise: Promise<FieldError[]> = null;
8079

80+
private watcherCenter = new WatcherCenter(this);
81+
8182
constructor(forceRootUpdate: () => void) {
8283
this.forceRootUpdate = forceRootUpdate;
8384
}
@@ -121,7 +122,6 @@ export class FormStore {
121122
setPreserve: this.setPreserve,
122123
getInitialValue: this.getInitialValue,
123124
registerWatch: this.registerWatch,
124-
setBatchUpdate: this.setBatchUpdate,
125125
};
126126
}
127127

@@ -195,47 +195,12 @@ export class FormStore {
195195
};
196196

197197
// ============================= Watch ============================
198-
private watchList: WatchCallBack[] = [];
199-
200198
private registerWatch: InternalHooks['registerWatch'] = callback => {
201-
this.watchList.push(callback);
202-
203-
return () => {
204-
this.watchList = this.watchList.filter(fn => fn !== callback);
205-
};
199+
return this.watcherCenter.register(callback);
206200
};
207201

208202
private notifyWatch = (namePath: InternalNamePath[] = []) => {
209-
// No need to cost perf when nothing need to watch
210-
if (this.watchList.length) {
211-
const values = this.getFieldsValue();
212-
const allValues = this.getFieldsValue(true);
213-
214-
this.watchList.forEach(callback => {
215-
callback(values, allValues, namePath);
216-
});
217-
}
218-
};
219-
220-
private notifyWatchNamePathList: InternalNamePath[] = [];
221-
private batchNotifyWatch = (namePath: InternalNamePath) => {
222-
this.notifyWatchNamePathList.push(namePath);
223-
this.batch('notifyWatch', () => {
224-
this.notifyWatch(this.notifyWatchNamePathList);
225-
this.notifyWatchNamePathList = [];
226-
});
227-
};
228-
229-
// ============================= Batch ============================
230-
private batchUpdate: BatchTask;
231-
232-
private setBatchUpdate = (batchUpdate: BatchTask) => {
233-
this.batchUpdate = batchUpdate;
234-
};
235-
236-
// Batch call the task, only last will be called
237-
private batch = (key: string, callback: VoidFunction) => {
238-
this.batchUpdate(key, callback);
203+
this.watcherCenter.notify(namePath);
239204
};
240205

241206
// ========================== Dev Warning =========================
@@ -669,7 +634,7 @@ export class FormStore {
669634
private registerField = (entity: FieldEntity) => {
670635
this.fieldEntities.push(entity);
671636
const namePath = entity.getNamePath();
672-
this.batchNotifyWatch(namePath);
637+
this.notifyWatch([namePath]);
673638

674639
// Set initial values
675640
if (entity.props.initialValue !== undefined) {
@@ -709,7 +674,7 @@ export class FormStore {
709674
}
710675
}
711676

712-
this.batchNotifyWatch(namePath);
677+
this.notifyWatch([namePath]);
713678
};
714679
};
715680

@@ -1078,6 +1043,7 @@ function useForm<Values = any>(form?: FormInstance<Values>): [FormInstance<Value
10781043
const formRef = React.useRef<FormInstance>(null);
10791044
const [, forceUpdate] = React.useState({});
10801045

1046+
// Create singleton FormStore
10811047
if (!formRef.current) {
10821048
if (form) {
10831049
formRef.current = form;

src/hooks/useNotifyWatch.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { matchNamePath } from '../utils/valueUtil';
2+
import type { InternalNamePath, WatchCallBack } from '../interface';
3+
import type { FormStore } from './useForm';
4+
5+
/**
6+
* Call action with delay in macro task.
7+
*/
8+
const macroTask = (fn: VoidFunction) => {
9+
const channel = new MessageChannel();
10+
channel.port1.onmessage = fn;
11+
channel.port2.postMessage(null);
12+
};
13+
14+
export default class WatcherCenter {
15+
namePathList: InternalNamePath[] = [];
16+
taskId: number = 0;
17+
18+
watcherList = new Set<WatchCallBack>();
19+
form: FormStore;
20+
21+
constructor(form: FormStore) {
22+
this.form = form;
23+
}
24+
25+
public register(callback: WatchCallBack): VoidFunction {
26+
this.watcherList.add(callback);
27+
28+
return () => {
29+
this.watcherList.delete(callback);
30+
};
31+
}
32+
33+
public notify(namePath: InternalNamePath[]) {
34+
// Insert with deduplication
35+
namePath.forEach(path => {
36+
if (this.namePathList.every(exist => !matchNamePath(exist, path))) {
37+
this.namePathList.push(path);
38+
}
39+
});
40+
41+
this.doBatch();
42+
}
43+
44+
private doBatch() {
45+
this.taskId += 1;
46+
const currentId = this.taskId;
47+
48+
macroTask(() => {
49+
if (currentId === this.taskId && this.watcherList.size) {
50+
const formInst = this.form.getForm();
51+
const values = formInst.getFieldsValue();
52+
const allValues = formInst.getFieldsValue(true);
53+
54+
this.watcherList.forEach(callback => {
55+
callback(values, allValues, this.namePathList);
56+
});
57+
58+
this.namePathList = [];
59+
}
60+
});
61+
}
62+
}

0 commit comments

Comments
 (0)