Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/demo/debug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## debug


<code src="../examples/debug.tsx"></code>
51 changes: 51 additions & 0 deletions docs/examples/debug.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>(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 (
<Form form={form}>
<div className="App">
{/* When I made the switch, it was very laggy */}
<button
onClick={() => {
setOpen(!open);
}}
>
Switch
</button>
<WatchCom name={0} />
<WatchCom name={1} />
{open ? (
<div className="flex gap-[5px] flex-wrap">
{data?.map(item => {
return (
<Field key={item.name} name={item.name}>
<input />
</Field>
);
})}
</div>
) : (
<h2>some thing</h2>
)}
</div>
</Form>
);
}
34 changes: 34 additions & 0 deletions src/BatchUpdate.tsx
Original file line number Diff line number Diff line change
@@ -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<BatchUpdateRef>((_, ref) => {
const [batchInfo, setBatchInfo] = React.useState<Record<string, VoidFunction>>({});

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;
1 change: 1 addition & 0 deletions src/FieldContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const Context = React.createContext<InternalFormInstance>({
setValidateMessages: warningFunc,
setPreserve: warningFunc,
getInitialValue: warningFunc,
setBatchUpdate: warningFunc,
};
},
});
Expand Down
40 changes: 40 additions & 0 deletions src/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<React.FormHTMLAttributes<HTMLFormElement>, 'onSubmit' | 'children'>;

Expand Down Expand Up @@ -70,6 +71,7 @@ const Form: React.ForwardRefRenderFunction<FormRef, FormProps> = (
setValidateMessages,
setPreserve,
destroyForm,
setBatchUpdate,
} = (formInstance as InternalFormInstance).getInternalHooks(HOOK_MARK);

// Pass ref with form instance
Expand Down Expand Up @@ -118,6 +120,42 @@ const Form: React.ForwardRefRenderFunction<FormRef, FormProps> = (
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<BatchUpdateRef>(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
Expand Down Expand Up @@ -146,6 +184,7 @@ const Form: React.ForwardRefRenderFunction<FormRef, FormProps> = (
prevFieldsRef.current = fields;
}, [fields, formInstance]);

// =========================== Render ===========================
const formContextValue = React.useMemo(
() => ({
...(formInstance as InternalFormInstance),
Expand All @@ -157,6 +196,7 @@ const Form: React.ForwardRefRenderFunction<FormRef, FormProps> = (
const wrapperNode = (
<ListContext.Provider value={null}>
<FieldContext.Provider value={formContextValue}>{childrenNode}</FieldContext.Provider>
<BatchUpdate ref={setBatchUpdateRef} />
</ListContext.Provider>
);

Expand Down
2 changes: 2 additions & 0 deletions src/interface.ts
Original file line number Diff line number Diff line change
@@ -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<T = any> = DeepNamePath<T>;
Expand Down Expand Up @@ -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 */
Expand Down
27 changes: 25 additions & 2 deletions src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
matchNamePath,
setValue,
} from './utils/valueUtil';
import { BatchTask, BatchUpdateRef } from './BatchUpdate';

type InvalidateFieldEntity = { INVALIDATE_NAME_PATH: InternalNamePath };

Expand Down Expand Up @@ -119,6 +120,7 @@ export class FormStore {
setPreserve: this.setPreserve,
getInitialValue: this.getInitialValue,
registerWatch: this.registerWatch,
setBatchUpdate: this.setBatchUpdate,
};
}

Expand Down Expand Up @@ -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);
};
Comment on lines +229 to +238
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

批量更新实现需要添加空值检查。

batch 方法直接调用 this.batchUpdate(key, callback),但 batchUpdate 在初始化时可能为 undefined,这会导致运行时错误。

建议添加空值检查:

 // Batch call the task, only last will be called
 private batch = (key: string, callback: VoidFunction) => {
+  if (!this.batchUpdate) {
+    // 如果批量更新函数尚未设置,直接执行回调
+    callback();
+    return;
+  }
   this.batchUpdate(key, callback);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
};
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) => {
if (!this.batchUpdate) {
// 如果批量更新函数尚未设置,直接执行回调
callback();
return;
}
this.batchUpdate(key, callback);
};
🤖 Prompt for AI Agents
In src/useForm.ts around lines 229 to 238, the batch method calls
this.batchUpdate without checking if it is defined, which can cause runtime
errors if batchUpdate is undefined. Add a null or undefined check before calling
this.batchUpdate in the batch method to ensure it is a valid function before
invocation.


// ========================== Dev Warning =========================
private timeoutId: any = null;

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -682,7 +705,7 @@ export class FormStore {
}
}

this.notifyWatch([namePath]);
this.batchNotifyWatch(namePath);
};
};

Expand Down
Loading