Skip to content
Open
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
64 changes: 45 additions & 19 deletions docs/examples/debug.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,53 @@ import Input from './components/Input';

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

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

return (
<Form form={form}>
<button
onClick={() => {
setKeyName(!keyName);
}}
>
Switch {String(keyName)}
</button>
<Field name="name" initialValue="bamboo">
<Input />
</Field>
<Field name="age" initialValue="light">
<Input />
</Field>
{val}
</Form>
<div
style={{
padding: 24,
border: '2px solid #1890ff',
borderRadius: 8,
marginBottom: 24,
}}
>
<h2 style={{ color: '#1890ff' }}>Antd V6 - useWatch + Form.List</h2>

<Form form={form} style={{ maxWidth: 600 }} initialValues={{
names: [
'aaa',
'bbb'
]
}}>
<Form.List name="names">
{(fields, { add, remove }) => {
return (
<>
{fields.map(({key, ...field}, index) => (
<div key={key}>
<Field {...field}>
<Input placeholder="用户名" style={{ width: 200 }} />
</Field>
<button type="button" onClick={() => remove(index)}>
删除
</button>
</div>
))}

<div>
<button type="button" onClick={() => add()}>
+ 添加用户
</button>
<button onClick={() => remove(1)}>删除索引 1</button>
</div>
</>
);
}}
</Form.List>
</Form>
</div>
);
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"gh-pages": "^6.1.0",
"jest": "^29.0.0",
"prettier": "^3.1.0",
"rc-test": "^7.0.15",
"rc-test": "^7.1.3",
"react": "^18.0.0",
"react-dnd": "^8.0.3",
"react-dnd-html5-backend": "^8.0.3",
Expand Down
34 changes: 0 additions & 34 deletions src/BatchUpdate.tsx

This file was deleted.

1 change: 0 additions & 1 deletion src/FieldContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ const Context = React.createContext<InternalFormInstance>({
setValidateMessages: warningFunc,
setPreserve: warningFunc,
getInitialValue: warningFunc,
setBatchUpdate: warningFunc,
};
},
});
Expand Down
41 changes: 1 addition & 40 deletions src/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,12 @@ import type {
InternalFormInstance,
FormRef,
} from './interface';
import useForm from './useForm';
import useForm from './hooks/useForm';
import FieldContext, { HOOK_MARK } from './FieldContext';
import type { FormContextProps } from './FormContext';
import FormContext from './FormContext';
import { isSimilar } from './utils/valueUtil';
import ListContext from './ListContext';
import type { BatchTask, BatchUpdateRef } from './BatchUpdate';
import BatchUpdate from './BatchUpdate';

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

Expand Down Expand Up @@ -72,7 +70,6 @@ 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 @@ -121,41 +118,6 @@ 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),
Expand Down Expand Up @@ -197,7 +159,6 @@ 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
62 changes: 14 additions & 48 deletions src/useForm.ts → src/hooks/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { merge } from '@rc-component/util/lib/utils/set';
import { mergeWith } from '@rc-component/util';
import warning from '@rc-component/util/lib/warning';
import * as React from 'react';
import { HOOK_MARK } from './FieldContext';
import { HOOK_MARK } from '../FieldContext';
import type {
Callbacks,
FieldData,
Expand All @@ -26,20 +26,19 @@ import type {
ValidateErrorEntity,
ValidateMessages,
ValuedNotifyInfo,
WatchCallBack,
} from './interface';
import { allPromiseFinish } from './utils/asyncUtil';
import { defaultValidateMessages } from './utils/messages';
import NameMap from './utils/NameMap';
} from '../interface';
import { allPromiseFinish } from '../utils/asyncUtil';
import { defaultValidateMessages } from '../utils/messages';
import NameMap from '../utils/NameMap';
import {
cloneByNamePathList,
containsNamePath,
getNamePath,
getValue,
matchNamePath,
setValue,
} from './utils/valueUtil';
import type { BatchTask } from './BatchUpdate';
} from '../utils/valueUtil';
import WatcherCenter from './useNotifyWatch';

type FlexibleFieldEntity = Partial<FieldEntity>;

Expand Down Expand Up @@ -78,6 +77,8 @@ export class FormStore {

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

private watcherCenter = new WatcherCenter(this);

constructor(forceRootUpdate: () => void) {
this.forceRootUpdate = forceRootUpdate;
}
Expand Down Expand Up @@ -121,7 +122,6 @@ export class FormStore {
setPreserve: this.setPreserve,
getInitialValue: this.getInitialValue,
registerWatch: this.registerWatch,
setBatchUpdate: this.setBatchUpdate,
};
}

Expand Down Expand Up @@ -195,47 +195,12 @@ export class FormStore {
};

// ============================= Watch ============================
private watchList: WatchCallBack[] = [];

private registerWatch: InternalHooks['registerWatch'] = callback => {
this.watchList.push(callback);

return () => {
this.watchList = this.watchList.filter(fn => fn !== callback);
};
return this.watcherCenter.register(callback);
};

private notifyWatch = (namePath: InternalNamePath[] = []) => {
// No need to cost perf when nothing need to watch
if (this.watchList.length) {
const values = this.getFieldsValue();
const allValues = this.getFieldsValue(true);

this.watchList.forEach(callback => {
callback(values, allValues, namePath);
});
}
};

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);
this.watcherCenter.notify(namePath);
};

// ========================== Dev Warning =========================
Expand Down Expand Up @@ -669,7 +634,7 @@ export class FormStore {
private registerField = (entity: FieldEntity) => {
this.fieldEntities.push(entity);
const namePath = entity.getNamePath();
this.batchNotifyWatch(namePath);
this.notifyWatch([namePath]);

// Set initial values
if (entity.props.initialValue !== undefined) {
Expand Down Expand Up @@ -709,7 +674,7 @@ export class FormStore {
}
}

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

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

// Create singleton FormStore
if (!formRef.current) {
if (form) {
formRef.current = form;
Expand Down
62 changes: 62 additions & 0 deletions src/hooks/useNotifyWatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { matchNamePath } from '../utils/valueUtil';
import type { InternalNamePath, WatchCallBack } from '../interface';
import type { FormStore } from './useForm';

/**
* Call action with delay in macro task.
*/
const macroTask = (fn: VoidFunction) => {
const channel = new MessageChannel();
channel.port1.onmessage = fn;
channel.port2.postMessage(null);
};

export default class WatcherCenter {
namePathList: InternalNamePath[] = [];
taskId: number = 0;

watcherList = new Set<WatchCallBack>();
form: FormStore;

constructor(form: FormStore) {
this.form = form;
}

public register(callback: WatchCallBack): VoidFunction {
this.watcherList.add(callback);

return () => {
this.watcherList.delete(callback);
};
}

public notify(namePath: InternalNamePath[]) {
// Insert with deduplication
namePath.forEach(path => {
if (this.namePathList.every(exist => !matchNamePath(exist, path))) {
this.namePathList.push(path);
}
});

this.doBatch();
}

private doBatch() {
this.taskId += 1;
const currentId = this.taskId;

macroTask(() => {
if (currentId === this.taskId && this.watcherList.size) {
const formInst = this.form.getForm();
const values = formInst.getFieldsValue();
const allValues = formInst.getFieldsValue(true);

this.watcherList.forEach(callback => {
callback(values, allValues, this.namePathList);
});

this.namePathList = [];
}
});
}
}
Loading