Skip to content

Commit 4b12dd0

Browse files
authored
fix: batch remove laggy (#757)
* fix: batch remove laggy * chore: fix lint * chore: all batch * chore: fix lint
1 parent 6611c31 commit 4b12dd0

File tree

7 files changed

+157
-2
lines changed

7 files changed

+157
-2
lines changed

docs/demo/debug.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
## debug
2+
3+
4+
<code src="../examples/debug.tsx"></code>

docs/examples/debug.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import Form, { Field } from 'rc-field-form';
2+
import React from 'react';
3+
4+
function WatchCom({ name }: { name: number }) {
5+
const data = Form.useWatch(name);
6+
return data;
7+
}
8+
9+
export default function App() {
10+
const [form] = Form.useForm();
11+
const [open, setOpen] = React.useState<boolean>(true);
12+
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+
}, []);
21+
22+
return (
23+
<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>
49+
</Form>
50+
);
51+
}

src/BatchUpdate.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as React from 'react';
2+
3+
export type BatchTask = (key: string, callback: VoidFunction) => void;
4+
5+
export interface BatchUpdateRef {
6+
batch: BatchTask;
7+
}
8+
9+
const BatchUpdate = React.forwardRef<BatchUpdateRef>((_, ref) => {
10+
const [batchInfo, setBatchInfo] = React.useState<Record<string, VoidFunction>>({});
11+
12+
React.useLayoutEffect(() => {
13+
const keys = Object.keys(batchInfo);
14+
if (keys.length) {
15+
keys.forEach(key => {
16+
batchInfo[key]?.();
17+
});
18+
setBatchInfo({});
19+
}
20+
}, [batchInfo]);
21+
22+
React.useImperativeHandle(ref, () => ({
23+
batch: (key, callback) => {
24+
setBatchInfo(ori => ({
25+
...ori,
26+
[key]: callback,
27+
}));
28+
},
29+
}));
30+
31+
return null;
32+
});
33+
34+
export default BatchUpdate;

src/FieldContext.ts

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

src/Form.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { FormContextProps } from './FormContext';
1414
import FormContext from './FormContext';
1515
import { isSimilar } from './utils/valueUtil';
1616
import ListContext from './ListContext';
17+
import BatchUpdate, { BatchTask, type BatchUpdateRef } from './BatchUpdate';
1718

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

@@ -70,6 +71,7 @@ const Form: React.ForwardRefRenderFunction<FormRef, FormProps> = (
7071
setValidateMessages,
7172
setPreserve,
7273
destroyForm,
74+
setBatchUpdate,
7375
} = (formInstance as InternalFormInstance).getInternalHooks(HOOK_MARK);
7476

7577
// Pass ref with form instance
@@ -118,6 +120,42 @@ const Form: React.ForwardRefRenderFunction<FormRef, FormProps> = (
118120
mountRef.current = true;
119121
}
120122

123+
// ======================== Batch Update ========================
124+
// zombieJ:
125+
// To avoid Form self re-render,
126+
// We create a sub component `BatchUpdate` to handle batch update logic.
127+
// When the call with do not change immediate, we will batch the update
128+
// and flush it in `useLayoutEffect` for next tick.
129+
130+
// Set batch update ref
131+
const batchUpdateRef = React.useRef<BatchUpdateRef>(null);
132+
const batchUpdateTasksRef = React.useRef<[key: string, fn: VoidFunction][]>([]);
133+
134+
const tryFlushBatch = () => {
135+
if (batchUpdateRef.current) {
136+
batchUpdateTasksRef.current.forEach(([key, fn]) => {
137+
batchUpdateRef.current.batch(key, fn);
138+
});
139+
batchUpdateTasksRef.current = [];
140+
}
141+
};
142+
143+
// Ref update
144+
const setBatchUpdateRef = React.useCallback((batchUpdate: BatchUpdateRef | null) => {
145+
batchUpdateRef.current = batchUpdate;
146+
tryFlushBatch();
147+
}, []);
148+
149+
// Task list
150+
151+
const batchUpdate: BatchTask = (key, callback) => {
152+
batchUpdateTasksRef.current.push([key, callback]);
153+
tryFlushBatch();
154+
};
155+
156+
setBatchUpdate(batchUpdate);
157+
158+
// ========================== Unmount ===========================
121159
React.useEffect(
122160
() => () => destroyForm(clearOnDestroy),
123161
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -146,6 +184,7 @@ const Form: React.ForwardRefRenderFunction<FormRef, FormProps> = (
146184
prevFieldsRef.current = fields;
147185
}, [fields, formInstance]);
148186

187+
// =========================== Render ===========================
149188
const formContextValue = React.useMemo(
150189
() => ({
151190
...(formInstance as InternalFormInstance),
@@ -157,6 +196,7 @@ const Form: React.ForwardRefRenderFunction<FormRef, FormProps> = (
157196
const wrapperNode = (
158197
<ListContext.Provider value={null}>
159198
<FieldContext.Provider value={formContextValue}>{childrenNode}</FieldContext.Provider>
199+
<BatchUpdate ref={setBatchUpdateRef} />
160200
</ListContext.Provider>
161201
);
162202

src/interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ReactElement } from 'react';
22
import type { DeepNamePath } from './namePathType';
33
import type { ReducerAction } from './useForm';
4+
import type { BatchTask } from './BatchUpdate';
45

56
export type InternalNamePath = (string | number)[];
67
export type NamePath<T = any> = DeepNamePath<T>;
@@ -233,6 +234,7 @@ export interface InternalHooks {
233234
setValidateMessages: (validateMessages: ValidateMessages) => void;
234235
setPreserve: (preserve?: boolean) => void;
235236
getInitialValue: (namePath: InternalNamePath) => StoreValue;
237+
setBatchUpdate: (fn: BatchTask) => void;
236238
}
237239

238240
/** Only return partial when type is not any */

src/useForm.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
matchNamePath,
3939
setValue,
4040
} from './utils/valueUtil';
41+
import type { BatchTask } from './BatchUpdate';
4142

4243
type InvalidateFieldEntity = { INVALIDATE_NAME_PATH: InternalNamePath };
4344

@@ -119,6 +120,7 @@ export class FormStore {
119120
setPreserve: this.setPreserve,
120121
getInitialValue: this.getInitialValue,
121122
registerWatch: this.registerWatch,
123+
setBatchUpdate: this.setBatchUpdate,
122124
};
123125
}
124126

@@ -214,6 +216,27 @@ export class FormStore {
214216
}
215217
};
216218

219+
private notifyWatchNamePathList: InternalNamePath[] = [];
220+
private batchNotifyWatch = (namePath: InternalNamePath) => {
221+
this.notifyWatchNamePathList.push(namePath);
222+
this.batch('notifyWatch', () => {
223+
this.notifyWatch(this.notifyWatchNamePathList);
224+
this.notifyWatchNamePathList = [];
225+
});
226+
};
227+
228+
// ============================= Batch ============================
229+
private batchUpdate: BatchTask;
230+
231+
private setBatchUpdate = (batchUpdate: BatchTask) => {
232+
this.batchUpdate = batchUpdate;
233+
};
234+
235+
// Batch call the task, only last will be called
236+
private batch = (key: string, callback: VoidFunction) => {
237+
this.batchUpdate(key, callback);
238+
};
239+
217240
// ========================== Dev Warning =========================
218241
private timeoutId: any = null;
219242

@@ -642,7 +665,7 @@ export class FormStore {
642665
private registerField = (entity: FieldEntity) => {
643666
this.fieldEntities.push(entity);
644667
const namePath = entity.getNamePath();
645-
this.notifyWatch([namePath]);
668+
this.batchNotifyWatch(namePath);
646669

647670
// Set initial values
648671
if (entity.props.initialValue !== undefined) {
@@ -682,7 +705,7 @@ export class FormStore {
682705
}
683706
}
684707

685-
this.notifyWatch([namePath]);
708+
this.batchNotifyWatch(namePath);
686709
};
687710
};
688711

0 commit comments

Comments
 (0)