Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 type { BatchTask } 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