diff --git a/docs/demo/debug.md b/docs/demo/debug.md new file mode 100644 index 00000000..0edca525 --- /dev/null +++ b/docs/demo/debug.md @@ -0,0 +1,4 @@ +## debug + + + diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx new file mode 100644 index 00000000..3b3c7115 --- /dev/null +++ b/docs/examples/debug.tsx @@ -0,0 +1,51 @@ +import Form, { Field } from 'rc-field-form'; +import React from 'react'; + +function WatchCom({ name }: { name: number }) { + const data = Form.useWatch(name); + return data; +} + +export default function App() { + const [form] = Form.useForm(); + const [open, setOpen] = 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}`, + })); + }, []); + + return ( +
+
+ {/* When I made the switch, it was very laggy */} + + + + {open ? ( +
+ {data?.map(item => { + return ( + + + + ); + })} +
+ ) : ( +

some thing

+ )} +
+
+ ); +} diff --git a/src/BatchUpdate.tsx b/src/BatchUpdate.tsx new file mode 100644 index 00000000..9482dd04 --- /dev/null +++ b/src/BatchUpdate.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; + +export type BatchTask = (key: string, callback: VoidFunction) => void; + +export interface BatchUpdateRef { + batch: BatchTask; +} + +const BatchUpdate = React.forwardRef((_, ref) => { + const [batchInfo, setBatchInfo] = React.useState>({}); + + React.useLayoutEffect(() => { + const keys = Object.keys(batchInfo); + if (keys.length) { + keys.forEach(key => { + batchInfo[key]?.(); + }); + setBatchInfo({}); + } + }, [batchInfo]); + + React.useImperativeHandle(ref, () => ({ + batch: (key, callback) => { + setBatchInfo(ori => ({ + ...ori, + [key]: callback, + })); + }, + })); + + return null; +}); + +export default BatchUpdate; diff --git a/src/FieldContext.ts b/src/FieldContext.ts index d2b6f4d5..1f927949 100644 --- a/src/FieldContext.ts +++ b/src/FieldContext.ts @@ -42,6 +42,7 @@ const Context = React.createContext({ setValidateMessages: warningFunc, setPreserve: warningFunc, getInitialValue: warningFunc, + setBatchUpdate: warningFunc, }; }, }); diff --git a/src/Form.tsx b/src/Form.tsx index a883397c..601d1c87 100644 --- a/src/Form.tsx +++ b/src/Form.tsx @@ -14,6 +14,7 @@ import type { FormContextProps } from './FormContext'; import FormContext from './FormContext'; import { isSimilar } from './utils/valueUtil'; import ListContext from './ListContext'; +import BatchUpdate, { BatchTask, type BatchUpdateRef } from './BatchUpdate'; type BaseFormProps = Omit, 'onSubmit' | 'children'>; @@ -70,6 +71,7 @@ const Form: React.ForwardRefRenderFunction = ( setValidateMessages, setPreserve, destroyForm, + setBatchUpdate, } = (formInstance as InternalFormInstance).getInternalHooks(HOOK_MARK); // Pass ref with form instance @@ -118,6 +120,42 @@ const Form: React.ForwardRefRenderFunction = ( mountRef.current = true; } + // ======================== Batch Update ======================== + // zombieJ: + // To avoid Form self re-render, + // We create a sub component `BatchUpdate` to handle batch update logic. + // When the call with do not change immediate, we will batch the update + // and flush it in `useLayoutEffect` for next tick. + + // Set batch update ref + const batchUpdateRef = React.useRef(null); + const batchUpdateTasksRef = React.useRef<[key: string, fn: VoidFunction][]>([]); + + const tryFlushBatch = () => { + if (batchUpdateRef.current) { + batchUpdateTasksRef.current.forEach(([key, fn]) => { + batchUpdateRef.current.batch(key, fn); + }); + batchUpdateTasksRef.current = []; + } + }; + + // Ref update + const setBatchUpdateRef = React.useCallback((batchUpdate: BatchUpdateRef | null) => { + batchUpdateRef.current = batchUpdate; + tryFlushBatch(); + }, []); + + // Task list + + const batchUpdate: BatchTask = (key, callback) => { + batchUpdateTasksRef.current.push([key, callback]); + tryFlushBatch(); + }; + + setBatchUpdate(batchUpdate); + + // ========================== Unmount =========================== React.useEffect( () => () => destroyForm(clearOnDestroy), // eslint-disable-next-line react-hooks/exhaustive-deps @@ -146,6 +184,7 @@ const Form: React.ForwardRefRenderFunction = ( prevFieldsRef.current = fields; }, [fields, formInstance]); + // =========================== Render =========================== const formContextValue = React.useMemo( () => ({ ...(formInstance as InternalFormInstance), @@ -157,6 +196,7 @@ const Form: React.ForwardRefRenderFunction = ( const wrapperNode = ( {childrenNode} + ); diff --git a/src/interface.ts b/src/interface.ts index 482f6d09..fb8f0dde 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,6 +1,7 @@ import type { ReactElement } from 'react'; import type { DeepNamePath } from './namePathType'; import type { ReducerAction } from './useForm'; +import type { BatchTask } from './BatchUpdate'; export type InternalNamePath = (string | number)[]; export type NamePath = DeepNamePath; @@ -233,6 +234,7 @@ export interface InternalHooks { setValidateMessages: (validateMessages: ValidateMessages) => void; setPreserve: (preserve?: boolean) => void; getInitialValue: (namePath: InternalNamePath) => StoreValue; + setBatchUpdate: (fn: BatchTask) => void; } /** Only return partial when type is not any */ diff --git a/src/useForm.ts b/src/useForm.ts index cb9c5f09..fbebc2cf 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -38,6 +38,7 @@ import { matchNamePath, setValue, } from './utils/valueUtil'; +import type { BatchTask } from './BatchUpdate'; type InvalidateFieldEntity = { INVALIDATE_NAME_PATH: InternalNamePath }; @@ -119,6 +120,7 @@ export class FormStore { setPreserve: this.setPreserve, getInitialValue: this.getInitialValue, registerWatch: this.registerWatch, + setBatchUpdate: this.setBatchUpdate, }; } @@ -214,6 +216,27 @@ export class FormStore { } }; + private notifyWatchNamePathList: InternalNamePath[] = []; + private batchNotifyWatch = (namePath: InternalNamePath) => { + this.notifyWatchNamePathList.push(namePath); + this.batch('notifyWatch', () => { + this.notifyWatch(this.notifyWatchNamePathList); + this.notifyWatchNamePathList = []; + }); + }; + + // ============================= Batch ============================ + private batchUpdate: BatchTask; + + private setBatchUpdate = (batchUpdate: BatchTask) => { + this.batchUpdate = batchUpdate; + }; + + // Batch call the task, only last will be called + private batch = (key: string, callback: VoidFunction) => { + this.batchUpdate(key, callback); + }; + // ========================== Dev Warning ========================= private timeoutId: any = null; @@ -642,7 +665,7 @@ export class FormStore { private registerField = (entity: FieldEntity) => { this.fieldEntities.push(entity); const namePath = entity.getNamePath(); - this.notifyWatch([namePath]); + this.batchNotifyWatch(namePath); // Set initial values if (entity.props.initialValue !== undefined) { @@ -682,7 +705,7 @@ export class FormStore { } } - this.notifyWatch([namePath]); + this.batchNotifyWatch(namePath); }; };