diff --git a/examples/V7/parseFormatInputValues.tsx b/examples/V7/parseFormatInputValues.tsx
index 5bfd50497f4..d315f276694 100644
--- a/examples/V7/parseFormatInputValues.tsx
+++ b/examples/V7/parseFormatInputValues.tsx
@@ -24,7 +24,7 @@ export default function App() {
);
};
render();
+ await waitFor(() => {
+ screen.getByText('loading...');
+ });
+
await waitFor(() => {
expect((screen.getByRole('textbox') as HTMLInputElement).value).toEqual(
'test',
);
});
+
+ await waitFor(() => {
+ screen.getByText('done');
+ });
});
it('should update async default values for controlled components', async () => {
diff --git a/src/__tests__/useWatch.test.tsx b/src/__tests__/useWatch.test.tsx
index 13dbea707ac..1123dba6ead 100644
--- a/src/__tests__/useWatch.test.tsx
+++ b/src/__tests__/useWatch.test.tsx
@@ -92,6 +92,35 @@ describe('useWatch', () => {
expect(result.current).toEqual(['test', 'test1']);
});
+ it('should return own default value for single input', () => {
+ const { result } = renderHook(() => {
+ const { control } = useForm<{ test: string; test1: string }>({});
+ return useWatch({
+ control,
+ name: 'test',
+ defaultValue: 'test',
+ });
+ });
+
+ expect(result.current).toEqual('test');
+ });
+
+ it('should return own default value for array of inputs', () => {
+ const { result } = renderHook(() => {
+ const { control } = useForm<{ test: string; test1: string }>({});
+ return useWatch({
+ control,
+ name: ['test', 'test1'],
+ defaultValue: {
+ test: 'test',
+ test1: 'test1',
+ },
+ });
+ });
+
+ expect(result.current).toEqual(['test', 'test1']);
+ });
+
it('should return default value when name is undefined', () => {
const { result } = renderHook(() => {
const { control } = useForm<{ test: string; test1: string }>({
diff --git a/src/__typetest__/path/eager.test-d.ts b/src/__typetest__/path/eager.test-d.ts
index 4306bd0bc53..f53078cbf7a 100644
--- a/src/__typetest__/path/eager.test-d.ts
+++ b/src/__typetest__/path/eager.test-d.ts
@@ -23,6 +23,25 @@ import { _, Depth3Type } from '../__fixtures__';
const actual = _ as Path<{ foo: string[] }>;
expectType<'foo' | `foo.${number}`>(actual);
}
+
+ /** it should be able to avoid self-referencing/recursion, not crashing on self-referencing types. */ {
+ type Foo = { foo: Foo };
+ const actual = _ as Path;
+ expectType<'foo'>(actual);
+ }
+
+ /** it should not erroneously match subtypes as traversed */ {
+ type Foo =
+ | {
+ foo?: Foo;
+ bar?: {
+ baz: 1;
+ };
+ }
+ | {};
+ const actual = _ as Path;
+ expectType<'foo' | 'bar' | 'bar.baz'>(actual);
+ }
}
/** {@link ArrayPath} */ {
@@ -42,6 +61,25 @@ import { _, Depth3Type } from '../__fixtures__';
const actual = _ as ArrayPath<{ foo: string[][][] }>;
expectType<'foo' | `foo.${number}`>(actual);
}
+
+ /** it should be able to avoid self-referencing/recursion, not crashing on self-referencing types. */ {
+ type Foo = { foo: Foo[] };
+ const actual = _ as ArrayPath;
+ expectType<'foo'>(actual);
+ }
+
+ /** it should not erroneously match subtypes as traversed */ {
+ type Foo =
+ | {
+ bar?: {
+ baz?: 1;
+ fooArr?: Foo[];
+ };
+ }
+ | {};
+ const actual = _ as ArrayPath;
+ expectType<'bar.fooArr'>(actual);
+ }
}
/** {@link PathValue} */ {
diff --git a/src/logic/createFormControl.ts b/src/logic/createFormControl.ts
index ed11b568d76..ad75af5cb13 100644
--- a/src/logic/createFormControl.ts
+++ b/src/logic/createFormControl.ts
@@ -104,6 +104,7 @@ export function createFormControl<
let _formState: FormState = {
submitCount: 0,
isDirty: false,
+ isLoading: true,
isValidating: false,
isSubmitted: false,
isSubmitting: false,
@@ -1245,6 +1246,9 @@ export function createFormControl<
if (isFunction(_options.defaultValues)) {
_options.defaultValues().then((values) => {
reset(values, _options.resetOptions);
+ _subjects.state.next({
+ isLoading: false,
+ });
});
}
diff --git a/src/logic/validateField.ts b/src/logic/validateField.ts
index 2591f66f8dd..13e66e4bc1d 100644
--- a/src/logic/validateField.ts
+++ b/src/logic/validateField.ts
@@ -17,6 +17,7 @@ import isObject from '../utils/isObject';
import isRadioInput from '../utils/isRadioInput';
import isRegex from '../utils/isRegex';
import isString from '../utils/isString';
+import isUndefined from '../utils/isUndefined';
import appendErrors from './appendErrors';
import getCheckboxValue from './getCheckboxValue';
@@ -61,7 +62,9 @@ export default async (
const isCheckBox = isCheckBoxInput(ref);
const isRadioOrCheckbox = isRadio || isCheckBox;
const isEmpty =
- ((valueAsNumber || isFileInput(ref)) && !ref.value) ||
+ ((valueAsNumber || isFileInput(ref)) &&
+ isUndefined(ref.value) &&
+ isUndefined(inputValue)) ||
inputValue === '' ||
(Array.isArray(inputValue) && !inputValue.length);
const appendErrorsCurry = appendErrors.bind(
diff --git a/src/types/form.ts b/src/types/form.ts
index f6918217539..ee27bb6b8e6 100644
--- a/src/types/form.ts
+++ b/src/types/form.ts
@@ -130,6 +130,7 @@ export type ReadFormState = { [K in keyof FormStateProxy]: boolean | 'all' };
export type FormState = {
isDirty: boolean;
+ isLoading: boolean;
isSubmitted: boolean;
isSubmitSuccessful: boolean;
isSubmitting: boolean;
diff --git a/src/types/path/eager.ts b/src/types/path/eager.ts
index aff596df7f6..2ec8a040841 100644
--- a/src/types/path/eager.ts
+++ b/src/types/path/eager.ts
@@ -1,21 +1,58 @@
import { FieldValues } from '../fields';
import {
BrowserNativeObject,
+ IsAny,
+ IsEqual,
Primitive,
UnPackAsyncDefaultValues,
} from '../utils';
import { ArrayKey, IsTuple, TupleKeys } from './common';
+/**
+ * Helper function to break apart T1 and check if any are equal to T2
+ *
+ * See {@link IsEqual}
+ */
+type AnyIsEqual = T1 extends T2
+ ? IsEqual extends true
+ ? true
+ : never
+ : never;
+
/**
* Helper type for recursively constructing paths through a type.
+ * This actually constructs the strings and recurses into nested
+ * object types.
+ *
* See {@link Path}
*/
-type PathImpl = V extends
+type PathImpl = V extends
| Primitive
| BrowserNativeObject
? `${K}`
- : `${K}` | `${K}.${Path}`;
+ : // Check so that we don't recurse into the same type
+ // by ensuring that the types are mutually assignable
+ // mutually required to avoid false positives of subtypes
+ true extends AnyIsEqual
+ ? `${K}`
+ : `${K}` | `${K}.${PathInternal}`;
+
+/**
+ * Helper type for recursively constructing paths through a type.
+ * This obsucres the internal type param TraversedTypes from exported contract.
+ *
+ * See {@link Path}
+ */
+type PathInternal = T extends ReadonlyArray
+ ? IsTuple extends true
+ ? {
+ [K in TupleKeys]-?: PathImpl;
+ }[TupleKeys]
+ : PathImpl
+ : {
+ [K in keyof T]-?: PathImpl;
+ }[keyof T];
/**
* Type which eagerly collects all paths through a type
@@ -25,15 +62,9 @@ type PathImpl = V extends
* Path<{foo: {bar: string}}> = 'foo' | 'foo.bar'
* ```
*/
-export type Path = T extends ReadonlyArray
- ? IsTuple extends true
- ? {
- [K in TupleKeys]-?: PathImpl;
- }[TupleKeys]
- : PathImpl
- : {
- [K in keyof T]-?: PathImpl;
- }[keyof T];
+// We want to explode the union type and process each individually
+// so assignable types don't leak onto the stack from the base.
+export type Path = T extends any ? PathInternal : never;
/**
* See {@link Path}
@@ -44,36 +75,60 @@ export type FieldPath = Path<
/**
* Helper type for recursively constructing paths through a type.
+ * This actually constructs the strings and recurses into nested
+ * object types.
+ *
* See {@link ArrayPath}
*/
-type ArrayPathImpl = V extends
+type ArrayPathImpl = V extends
| Primitive
| BrowserNativeObject
- ? never
+ ? IsAny extends true
+ ? string
+ : never
: V extends ReadonlyArray
? U extends Primitive | BrowserNativeObject
+ ? IsAny extends true
+ ? string
+ : never
+ : // Check so that we don't recurse into the same type
+ // by ensuring that the types are mutually assignable
+ // mutually required to avoid false positives of subtypes
+ true extends AnyIsEqual
? never
- : `${K}` | `${K}.${ArrayPath}`
- : `${K}.${ArrayPath}`;
+ : `${K}` | `${K}.${ArrayPathInternal}`
+ : true extends AnyIsEqual
+ ? never
+ : `${K}.${ArrayPathInternal}`;
+
+/**
+ * Helper type for recursively constructing paths through a type.
+ * This obsucres the internal type param TraversedTypes from exported contract.
+ *
+ * See {@link ArrayPath}
+ */
+type ArrayPathInternal = T extends ReadonlyArray
+ ? IsTuple extends true
+ ? {
+ [K in TupleKeys]-?: ArrayPathImpl;
+ }[TupleKeys]
+ : ArrayPathImpl
+ : {
+ [K in keyof T]-?: ArrayPathImpl;
+ }[keyof T];
/**
* Type which eagerly collects all paths through a type which point to an array
* type.
- * @typeParam T - type which should be introspected
+ * @typeParam T - type which should be introspected.
* @example
* ```
* Path<{foo: {bar: string[], baz: number[]}}> = 'foo.bar' | 'foo.baz'
* ```
*/
-export type ArrayPath = T extends ReadonlyArray
- ? IsTuple extends true
- ? {
- [K in TupleKeys]-?: ArrayPathImpl;
- }[TupleKeys]
- : ArrayPathImpl
- : {
- [K in keyof T]-?: ArrayPathImpl;
- }[keyof T];
+// We want to explode the union type and process each individually
+// so assignable types don't leak onto the stack from the base.
+export type ArrayPath = T extends any ? ArrayPathInternal : never;
/**
* See {@link ArrayPath}
diff --git a/src/types/utils.ts b/src/types/utils.ts
index 073357414aa..edd688b7ed1 100644
--- a/src/types/utils.ts
+++ b/src/types/utils.ts
@@ -77,6 +77,26 @@ export type IsAny = 0 extends 1 & T ? true : false;
*/
export type IsNever = [T] extends [never] ? true : false;
+/**
+ * Checks whether T1 can be exactly (mutually) assigned to T2
+ * @typeParam T1 - type to check
+ * @typeParam T2 - type to check against
+ * ```
+ * IsEqual = true
+ * IsEqual<'foo', 'foo'> = true
+ * IsEqual = false
+ * IsEqual = false
+ * IsEqual = false
+ * IsEqual<'foo', string> = false
+ * IsEqual<'foo' | 'bar', 'foo'> = boolean // 'foo' is assignable, but 'bar' is not (true | false) -> boolean
+ * ```
+ */
+export type IsEqual = T1 extends T2
+ ? (() => G extends T1 ? 1 : 2) extends () => G extends T2 ? 1 : 2
+ ? true
+ : false
+ : false;
+
export type DeepMap = IsAny extends true
? any
: T extends BrowserNativeObject | NestedValue
diff --git a/src/types/validator.ts b/src/types/validator.ts
index d1a62067d31..741a09b66e8 100644
--- a/src/types/validator.ts
+++ b/src/types/validator.ts
@@ -15,7 +15,7 @@ export type ValidationValueMessage<
message: Message;
};
-export type ValidateResult = Message | Message[] | boolean | undefined;
+export type ValidateResult = Message | boolean | undefined;
export type Validate = (
value: TFieldValue,
diff --git a/src/useForm.ts b/src/useForm.ts
index 5a2f0f24ed6..c22591a99c3 100644
--- a/src/useForm.ts
+++ b/src/useForm.ts
@@ -49,6 +49,7 @@ export function useForm<
const [formState, updateFormState] = React.useState>({
isDirty: false,
isValidating: false,
+ isLoading: true,
isSubmitted: false,
isSubmitting: false,
isSubmitSuccessful: false,
diff --git a/src/useFormState.ts b/src/useFormState.ts
index ee1bc10a0ae..c73cdc3fe10 100644
--- a/src/useFormState.ts
+++ b/src/useFormState.ts
@@ -51,6 +51,7 @@ function useFormState(
const _mounted = React.useRef(true);
const _localProxyFormState = React.useRef({
isDirty: false,
+ isLoading: false,
dirtyFields: false,
touchedFields: false,
isValidating: false,
diff --git a/src/useWatch.ts b/src/useWatch.ts
index 355f348ff9c..2881af8cc00 100644
--- a/src/useWatch.ts
+++ b/src/useWatch.ts
@@ -181,9 +181,10 @@ export function useWatch(
});
const [value, updateValue] = React.useState(
- isUndefined(defaultValue)
- ? control._getWatch(name as InternalFieldName)
- : defaultValue,
+ control._getWatch(
+ name as InternalFieldName,
+ defaultValue as DeepPartialSkipArrayKey,
+ ),
);
React.useEffect(() => control._removeUnmounted());
diff --git a/src/utils/unset.ts b/src/utils/unset.ts
index 2f777db3ba3..766309b1d02 100644
--- a/src/utils/unset.ts
+++ b/src/utils/unset.ts
@@ -24,41 +24,28 @@ function isEmptyArray(obj: unknown[]) {
return true;
}
-export default function unset(object: any, path: string) {
- const updatePath = isKey(path) ? [path] : stringToPath(path);
- const childObject =
- updatePath.length == 1 ? object : baseGet(object, updatePath);
- const key = updatePath[updatePath.length - 1];
- let previousObjRef;
+export default function unset(object: any, path: string | (string | number)[]) {
+ const paths = Array.isArray(path)
+ ? path
+ : isKey(path)
+ ? [path]
+ : stringToPath(path);
+
+ const childObject = paths.length === 1 ? object : baseGet(object, paths);
+
+ const index = paths.length - 1;
+ const key = paths[index];
if (childObject) {
delete childObject[key];
}
- for (let k = 0; k < updatePath.slice(0, -1).length; k++) {
- let index = -1;
- let objectRef;
- const currentPaths = updatePath.slice(0, -(k + 1));
- const currentPathsLength = currentPaths.length - 1;
-
- if (k > 0) {
- previousObjRef = object;
- }
-
- while (++index < currentPaths.length) {
- const item = currentPaths[index];
- objectRef = objectRef ? objectRef[item] : object[item];
-
- if (
- currentPathsLength === index &&
- ((isObject(objectRef) && isEmptyObject(objectRef)) ||
- (Array.isArray(objectRef) && isEmptyArray(objectRef)))
- ) {
- previousObjRef ? delete previousObjRef[item] : delete object[item];
- }
-
- previousObjRef = objectRef;
- }
+ if (
+ index !== 0 &&
+ ((isObject(childObject) && isEmptyObject(childObject)) ||
+ (Array.isArray(childObject) && isEmptyArray(childObject)))
+ ) {
+ unset(object, paths.slice(0, -1));
}
return object;