diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx
index 4a957f4c..a52e6fb2 100644
--- a/docs/examples/debug.tsx
+++ b/docs/examples/debug.tsx
@@ -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 (
-
+
+
Antd V6 - useWatch + Form.List
+
+
+ {(fields, { add, remove }) => {
+ return (
+ <>
+ {fields.map(({key, ...field}, index) => (
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ >
+ );
+ }}
+
+
+
);
}
diff --git a/package.json b/package.json
index 6837d60d..bc50f56c 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/BatchUpdate.tsx b/src/BatchUpdate.tsx
deleted file mode 100644
index 9482dd04..00000000
--- a/src/BatchUpdate.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-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 1f927949..d2b6f4d5 100644
--- a/src/FieldContext.ts
+++ b/src/FieldContext.ts
@@ -42,7 +42,6 @@ const Context = React.createContext({
setValidateMessages: warningFunc,
setPreserve: warningFunc,
getInitialValue: warningFunc,
- setBatchUpdate: warningFunc,
};
},
});
diff --git a/src/Form.tsx b/src/Form.tsx
index 184cc2d5..84a4f62d 100644
--- a/src/Form.tsx
+++ b/src/Form.tsx
@@ -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, 'onSubmit' | 'children'>;
@@ -72,7 +70,6 @@ const Form: React.ForwardRefRenderFunction = (
setValidateMessages,
setPreserve,
destroyForm,
- setBatchUpdate,
} = (formInstance as InternalFormInstance).getInternalHooks(HOOK_MARK);
// Pass ref with form instance
@@ -121,41 +118,6 @@ 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),
@@ -197,7 +159,6 @@ const Form: React.ForwardRefRenderFunction = (
const wrapperNode = (
{childrenNode}
-
);
diff --git a/src/useForm.ts b/src/hooks/useForm.ts
similarity index 95%
rename from src/useForm.ts
rename to src/hooks/useForm.ts
index e2cd4b70..3353d8e6 100644
--- a/src/useForm.ts
+++ b/src/hooks/useForm.ts
@@ -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,
@@ -26,11 +26,10 @@ 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,
@@ -38,8 +37,8 @@ import {
getValue,
matchNamePath,
setValue,
-} from './utils/valueUtil';
-import type { BatchTask } from './BatchUpdate';
+} from '../utils/valueUtil';
+import WatcherCenter from './useNotifyWatch';
type FlexibleFieldEntity = Partial;
@@ -78,6 +77,8 @@ export class FormStore {
private lastValidatePromise: Promise = null;
+ private watcherCenter = new WatcherCenter(this);
+
constructor(forceRootUpdate: () => void) {
this.forceRootUpdate = forceRootUpdate;
}
@@ -121,7 +122,6 @@ export class FormStore {
setPreserve: this.setPreserve,
getInitialValue: this.getInitialValue,
registerWatch: this.registerWatch,
- setBatchUpdate: this.setBatchUpdate,
};
}
@@ -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 =========================
@@ -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) {
@@ -709,7 +674,7 @@ export class FormStore {
}
}
- this.batchNotifyWatch(namePath);
+ this.notifyWatch([namePath]);
};
};
@@ -1078,6 +1043,7 @@ function useForm(form?: FormInstance): [FormInstance(null);
const [, forceUpdate] = React.useState({});
+ // Create singleton FormStore
if (!formRef.current) {
if (form) {
formRef.current = form;
diff --git a/src/hooks/useNotifyWatch.ts b/src/hooks/useNotifyWatch.ts
new file mode 100644
index 00000000..5beea99e
--- /dev/null
+++ b/src/hooks/useNotifyWatch.ts
@@ -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();
+ 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 = [];
+ }
+ });
+ }
+}
diff --git a/src/useWatch.ts b/src/hooks/useWatch.ts
similarity index 96%
rename from src/useWatch.ts
rename to src/hooks/useWatch.ts
index 74b10176..c656ef53 100644
--- a/src/useWatch.ts
+++ b/src/hooks/useWatch.ts
@@ -1,15 +1,15 @@
import warning from '@rc-component/util/lib/warning';
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
-import FieldContext, { HOOK_MARK } from './FieldContext';
+import FieldContext, { HOOK_MARK } from '../FieldContext';
import type {
FormInstance,
InternalFormInstance,
NamePath,
Store,
WatchOptions,
-} from './interface';
-import { isFormInstance } from './utils/typeUtil';
-import { getNamePath, getValue } from './utils/valueUtil';
+} from '../interface';
+import { isFormInstance } from '../utils/typeUtil';
+import { getNamePath, getValue } from '../utils/valueUtil';
import { useEvent } from '@rc-component/util';
type ReturnPromise = T extends Promise ? ValueType : never;
diff --git a/src/index.tsx b/src/index.tsx
index bf535b72..4433aa77 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,13 +2,13 @@ import * as React from 'react';
import type { FormRef, FormInstance } from './interface';
import Field from './Field';
import List from './List';
-import useForm from './useForm';
+import useForm from './hooks/useForm';
import type { FormProps } from './Form';
import FieldForm from './Form';
import { FormProvider } from './FormContext';
import FieldContext from './FieldContext';
import ListContext from './ListContext';
-import useWatch from './useWatch';
+import useWatch from './hooks/useWatch';
const InternalForm = React.forwardRef(FieldForm) as (
props: FormProps & { ref?: React.Ref> },
diff --git a/src/interface.ts b/src/interface.ts
index b8f30b73..926093b0 100644
--- a/src/interface.ts
+++ b/src/interface.ts
@@ -240,7 +240,6 @@ 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/utils/valueUtil.ts b/src/utils/valueUtil.ts
index 21c123d4..9f4e6426 100644
--- a/src/utils/valueUtil.ts
+++ b/src/utils/valueUtil.ts
@@ -48,7 +48,7 @@ export function containsNamePath(
* Check if `namePath` is super set or equal of `subNamePath`.
* @param namePath A list of `InternalNamePath[]`
* @param subNamePath Compare `InternalNamePath`
- * @param partialMatch True will make `[a, b]` match `[a, b, c]`
+ * @param partialMatch Default false. True will make `[a, b]` match `[a, b, c]`
*/
export function matchNamePath(
namePath: InternalNamePath,
diff --git a/tests/common/index.ts b/tests/common/index.ts
index 23f03605..7f3b43f6 100644
--- a/tests/common/index.ts
+++ b/tests/common/index.ts
@@ -24,18 +24,26 @@ export function getInput(
return ele!;
}
+const nativeSetTimeout = window.setTimeout;
+
export async function changeValue(wrapper: HTMLElement, value: string | string[]) {
const values = Array.isArray(value) ? value : [value];
+ const isMockTimer = nativeSetTimeout !== window.setTimeout;
+
for (let i = 0; i < values.length; i += 1) {
fireEvent.change(wrapper, { target: { value: values[i] } });
- await act(async () => {
- await timeout();
- });
+ if (isMockTimer) {
+ act(() => {
+ jest.advanceTimersByTime(1000);
+ });
+ } else {
+ await act(async () => {
+ await timeout();
+ });
+ }
}
-
- return;
}
export function matchError(
diff --git a/tests/dependencies.test.tsx b/tests/dependencies.test.tsx
index a06facd3..deeb0fc7 100644
--- a/tests/dependencies.test.tsx
+++ b/tests/dependencies.test.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import type { FormInstance } from '../src';
import Form, { Field } from '../src';
-import timeout from './common/timeout';
+import timeout, { waitFakeTime } from './common/timeout';
import InfoField, { Input } from './common/InfoField';
import { changeValue, matchError, getInput } from './common';
import { fireEvent, render } from '@testing-library/react';
@@ -100,6 +100,8 @@ describe('Form.Dependencies', () => {
});
it('should work when field is dirty', async () => {
+ jest.useFakeTimers();
+
let pass = false;
const { container } = render(
@@ -135,8 +137,7 @@ describe('Form.Dependencies', () => {
);
fireEvent.submit(container.querySelector('form')!);
- await timeout();
- // wrapper.update();
+ await waitFakeTime();
matchError(getInput(container, 0, true), 'You should not pass');
// Mock new validate
@@ -149,6 +150,8 @@ describe('Form.Dependencies', () => {
fireEvent.click(container.querySelector('button')!);
await changeValue(getInput(container, 1), 'light');
matchError(getInput(container, 0, true), false);
+
+ jest.useRealTimers();
});
it('should work as a shortcut when name is not provided', async () => {
diff --git a/tests/index.test.tsx b/tests/index.test.tsx
index 24c37a6d..526ba868 100644
--- a/tests/index.test.tsx
+++ b/tests/index.test.tsx
@@ -3,9 +3,9 @@ import { resetWarned } from '@rc-component/util/lib/warning';
import React from 'react';
import type { FormInstance } from '../src';
import Form, { Field, useForm } from '../src';
-import { changeValue, getInput, matchError } from './common';
+import { changeValue, getInput, matchError, waitFakeTimer } from './common';
import InfoField, { Input } from './common/InfoField';
-import timeout from './common/timeout';
+import timeout, { waitFakeTime } from './common/timeout';
import type { FormRef, Meta } from '@/interface';
describe('Form.Basic', () => {
@@ -311,6 +311,8 @@ describe('Form.Basic', () => {
expect(getInput(container).value).toEqual('');
});
it('submit', async () => {
+ jest.useFakeTimers();
+
const onFinish = jest.fn();
const onFinishFailed = jest.fn();
@@ -324,7 +326,7 @@ describe('Form.Basic', () => {
// Not trigger
fireEvent.submit(container.querySelector('form'));
- await timeout();
+ await waitFakeTime();
matchError(container, "'user' is required");
expect(onFinish).not.toHaveBeenCalled();
expect(onFinishFailed).toHaveBeenCalledWith({
@@ -332,7 +334,7 @@ describe('Form.Basic', () => {
errorFields: [{ name: ['user'], errors: ["'user' is required"], warnings: [] }],
outOfDate: false,
values: {
- user: undefined
+ user: undefined,
},
});
@@ -342,10 +344,12 @@ describe('Form.Basic', () => {
// Trigger
await changeValue(getInput(container), 'Bamboo');
fireEvent.submit(container.querySelector('form'));
- await timeout();
+ await waitFakeTime();
matchError(container, false);
expect(onFinish).toHaveBeenCalledWith({ user: 'Bamboo' });
expect(onFinishFailed).not.toHaveBeenCalled();
+
+ jest.useRealTimers();
});
it('getInternalHooks should not usable by user', () => {
@@ -903,7 +907,7 @@ describe('Form.Basic', () => {
// (setFieldValue internally calls setFields with touched: true)
expect(formRef.current.isFieldTouched(['list', 1])).toBeTruthy();
expect(formRef.current.isFieldTouched(['nest', 'target'])).toBeTruthy();
-
+
// Verify other fields remain untouched
expect(formRef.current.isFieldTouched(['list', 0])).toBeFalsy();
expect(formRef.current.isFieldTouched(['list', 2])).toBeFalsy();
diff --git a/tests/setupAfterEnv.ts b/tests/setupAfterEnv.ts
index 7b0828bf..58503f88 100644
--- a/tests/setupAfterEnv.ts
+++ b/tests/setupAfterEnv.ts
@@ -1 +1,30 @@
import '@testing-library/jest-dom';
+
+window.MessageChannel = class {
+ port1: any;
+ port2: any;
+
+ constructor() {
+ const createPort = () => {
+ const port = {
+ onmessage: null,
+ postMessage: (message: any) => {
+ setTimeout(() => {
+ if (port._target && typeof port._target.onmessage === 'function') {
+ port._target.onmessage({ data: message });
+ }
+ }, 10);
+ },
+ _target: null,
+ };
+ return port;
+ };
+
+ const port1 = createPort();
+ const port2 = createPort();
+ port1._target = port2;
+ port2._target = port1;
+ this.port1 = port1;
+ this.port2 = port2;
+ }
+} as any;
diff --git a/tests/useWatch.test.tsx b/tests/useWatch.test.tsx
index b26dfcf1..ac6080d3 100644
--- a/tests/useWatch.test.tsx
+++ b/tests/useWatch.test.tsx
@@ -3,12 +3,20 @@ import { render, fireEvent, act } from '@testing-library/react';
import type { FormInstance } from '../src';
import { List } from '../src';
import Form, { Field } from '../src';
-import timeout from './common/timeout';
+import { waitFakeTime } from './common/timeout';
import { Input } from './common/InfoField';
-import { stringify } from '../src/useWatch';
+import { stringify } from '../src/hooks/useWatch';
import { changeValue } from './common';
describe('useWatch', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
it('field initialValue', async () => {
const Demo: React.FC = () => {
const [form] = Form.useForm();
@@ -26,9 +34,7 @@ describe('useWatch', () => {
};
const { container } = render();
- await act(async () => {
- await timeout();
- });
+ await waitFakeTime();
expect(container.querySelector('.values')?.textContent).toEqual('bamboo');
});
@@ -49,9 +55,7 @@ describe('useWatch', () => {
};
const { container } = render();
- await act(async () => {
- await timeout();
- });
+ await waitFakeTime();
expect(container.querySelector('.values')?.textContent).toEqual('bamboo');
});
@@ -73,23 +77,24 @@ describe('useWatch', () => {
};
const { container } = render();
- await act(async () => {
- await timeout();
- });
+ await waitFakeTime();
await act(async () => {
staticForm.current?.setFields([{ name: 'name', value: 'little' }]);
});
+ await waitFakeTime();
expect(container.querySelector('.values').textContent)?.toEqual('little');
await act(async () => {
staticForm.current?.setFieldsValue({ name: 'light' });
});
+ await waitFakeTime();
expect(container.querySelector('.values').textContent)?.toEqual('light');
await act(async () => {
staticForm.current?.resetFields();
});
+ await waitFakeTime();
expect(container.querySelector('.values').textContent)?.toEqual('');
});
@@ -113,17 +118,16 @@ describe('useWatch', () => {
};
const { container, rerender } = render();
-
- await act(async () => {
- await timeout();
- });
+ await waitFakeTime();
expect(container.querySelector('.values')?.textContent).toEqual('bamboo');
rerender();
+ await waitFakeTime();
expect(container.querySelector('.values')?.textContent).toEqual('');
rerender();
+ await waitFakeTime();
expect(container.querySelector('.values')?.textContent).toEqual('bamboo');
});
@@ -152,15 +156,15 @@ describe('useWatch', () => {
};
const { container, rerender } = render();
- await act(async () => {
- await timeout();
- });
+ await waitFakeTime();
expect(container.querySelector('.values')?.textContent).toEqual('bamboo');
rerender();
+ await waitFakeTime();
expect(container.querySelector('.values')?.textContent).toEqual('');
rerender();
+ await waitFakeTime();
expect(container.querySelector('.values')?.textContent).toEqual('bamboo');
});
});
@@ -195,16 +199,14 @@ describe('useWatch', () => {
};
const { container } = render();
- await act(async () => {
- await timeout();
- });
+ await waitFakeTime();
+
expect(container.querySelector('.values')?.textContent).toEqual(
JSON.stringify(['bamboo', 'light']),
);
fireEvent.click(container.querySelector('.remove'));
- await act(async () => {
- await timeout();
- });
+ await waitFakeTime();
+
expect(container.querySelector('.values')?.textContent).toEqual(
JSON.stringify(['light']),
);
@@ -350,9 +352,7 @@ describe('useWatch', () => {
};
const { container } = render();
- fireEvent.change(container.querySelector('input'), {
- target: { value: 'bamboo' },
- });
+ changeValue(container.querySelector('input'), 'bamboo');
expect(updateA > updateB).toBeTruthy();
});
@@ -382,10 +382,9 @@ describe('useWatch', () => {
);
};
const { container } = render();
- fireEvent.change(container.querySelector('input'), {
- target: { value: 'bamboo' },
- });
+ changeValue(container.querySelector('input'), 'bamboo');
container.querySelector('button').click();
+
expect(container.querySelector('.value')?.textContent).toEqual('bamboo');
});
it('stringify error', () => {
@@ -416,9 +415,10 @@ describe('useWatch', () => {
expect(container.querySelector('.value')?.textContent).toEqual('');
fireEvent.click(container.querySelector('.setUpdate'));
expect(container.querySelector('.value')?.textContent).toEqual('default');
- fireEvent.change(container.querySelector('input'), {
- target: { value: 'bamboo' },
- });
+ // fireEvent.change(container.querySelector('input'), {
+ // target: { value: 'bamboo' },
+ // });
+ changeValue(container.querySelector('input'), 'bamboo');
expect(container.querySelector('.value')?.textContent).toEqual('bamboo');
expect(errorSpy).not.toHaveBeenCalledWith(
'Warning: useWatch requires a form instance since it can not auto detect from context.',
@@ -481,14 +481,11 @@ describe('useWatch', () => {
};
const { container } = render();
- await act(async () => {
- await timeout();
- });
+ await waitFakeTime();
expect(logSpy).toHaveBeenCalledWith('bamboo', undefined); // initialValue
+
fireEvent.click(container.querySelector('.test-btn'));
- await act(async () => {
- await timeout();
- });
+ await waitFakeTime();
expect(logSpy).toHaveBeenCalledWith('light', undefined); // after setFieldValue
logSpy.mockRestore();
@@ -510,9 +507,8 @@ describe('useWatch', () => {
};
const { container } = render();
- await act(async () => {
- await timeout();
- });
+ await waitFakeTime();
+
expect(container.querySelector('.values')?.textContent).toEqual('bamboo');
const input = container.querySelectorAll('input');
await changeValue(input[0], 'bamboo2');
@@ -536,4 +532,47 @@ describe('useWatch', () => {
expect(list[0]).toEqual({});
expect(list[1]).toEqual({ name: 'bamboo' });
});
+
+ it('list remove should not trigger intermediate undefined value', async () => {
+ let snapshots: any[] = [];
+
+ const Demo: React.FC = () => {
+ const [form] = Form.useForm();
+ const users = Form.useWatch(['users'], form) || [];
+ snapshots.push(users);
+
+ return (
+
+ );
+ };
+
+ const { container } = render();
+ await waitFakeTime();
+ snapshots = [];
+
+ fireEvent.click(container.querySelector('button'));;
+ await waitFakeTime();
+
+ expect(snapshots).toHaveLength(1);
+ expect(snapshots[0]).toEqual(['bamboo']);
+ });
});
diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx
index 2a9d9227..7fb1cdd9 100644
--- a/tests/validate.test.tsx
+++ b/tests/validate.test.tsx
@@ -288,6 +288,8 @@ describe('Form.Validate', () => {
});
it('form context', async () => {
+ jest.useFakeTimers();
+
const { container, rerender } = render(