- Revert notify all matching field-array roots on nested
setValueupdates - Revert treat
NaNas empty whenvalueAsNumberistrueinvalidateField setValuespassoptionsparameter through to enable validationsetValuesemit whole-form change without stalename/type
setValuesskip redundant per-field deep clonessetValuesthreadskipClonethroughsetFieldValue
- Improve
isDirtysync withdirtyFieldsstate
- Preserve
formState.defaultValueswhenuseFieldArrayandwatchare used together - Preserve nested resolver field-array errors in
trigger() - Notify all matching field-array roots on nested
setValueupdates useFieldArrayremoveleaves array with empty object when usingvaluesprop- Preserve reset values for conditionally mounted
Controllerfields withshouldUnregister - Propagate
setValuesupdates to mountedControllerfields - Native validation tooltip suppression caused by duplicate submit-error focus
append({ obj: null })silently replaced bydefaultValuesafterremove()- Errors state when using form-level validation
isValidatingreactivity whenvalidatingFieldsis not subscribed
- Improve
getDirtyFieldsto prune empty fields - TypeScript 6.0 support
- Include
setValuesinFormProvidercontext value - Preserve watch updates on field array unmount
- Prevent
useWatchre-render when unrelated field validation occurs - Recompute
isDirtyafter re-registering a previously unregistered field
setValuesAPI
- Preserve previous field value when
useControllername changes - Handle null parent when unregistering nested field
- Treat
NaNas empty whenvalueAsNumberistrueinvalidateField
- Reverted
setValuesthat was accidentally included in patch; fix build to exclude test files
- Improve
subscribeAPI performance
- Memoize submit handler
- Improve
deepEqualperformance
- Safely access
field._fduring register - Improve
fieldStateerrors when resolver uses dot-notation string keys - Update state correctly in watch callback with
Controller,trigger, andreset - Skip field array validation when mode is
onBlur isDirtyremains false after deleting an item withshouldDirty: true- Handle nested field when parent
defaultValueis null - Skip re-render in
setValuewhen value is unchanged
- Prevent
setValuewithshouldDirtyfrom polluting unrelated dirty fields - Memoize control in
HookFormControlContextto prevent render conflicts isNameInFieldArrayshould check all ancestor paths for nested field arraysformState.isValidincorrect onControllerre-mount
- Built-in form-level
validateoption - Subscribe
formStateto track submit state
- Checkbox form validation ignored with native validation
- Prevent
useFieldArrayfrom marking unrelated fields as dirty
clearErrorsemit name signal for targeted field updates- Use
DeepPartialSkipArrayKeyforWatchObservervalue parameter
- Issue with
booleans_as_integersvalue handling
- Separate control context to prevent unnecessary rerenders
- Memoize
FormProvidercontext value to prevent unnecessary rerenders
- Update
isValidwhen fielddisabledstate changes
- Prevent field array ghost elements with
keepDirtyValues - Improve
watchreturn types - Improve invalid date handling in
deepEqualand validation - Handle branded types correctly in
DeepPartial - Fix native validation focus issue
- Prevent duplicate subscription trigger in
setValue
- Align
<Watch />API withuseWatch
- Security: CVE-2025-67779, CVE-2025-55184, CVE-2025-55183, CVE-2025-55182
- Preserve
isValidstate whenkeepIsValidoption is used - Ensure
createFormControl.subscribesubscription listens only to subscribed changes - Batch
isValidatingstate updates with validation result - Resolve race condition between
setErrorandsetFocus
<FormStateSubscribe />component
- Clear validation errors synchronously in
reset()to fix Next.js 16 Server Actions issue
exactoption foruseControllerprops
- Allow
undefinedvalue with asyncdefaultValuesinController - Correct
PathValueImpltype inference
- Reduce redundant property access in
getDirtyFields
- Skip
setValid()during batch array updates - Recompute
isValidafterresetwhen values update asynchronously - Handle NaN comparison correctly using
Object.isindeepEqual
useWatchanduseControllernow react tonameprop changes
watch()returningundefinedimmediately afterreset()- Correct render function parameter typing for
<Watch />component
<Watch />component
- Respect parent-provided
useFieldArrayrules getDirtyFieldssubmit fields withnullvalues when usinguseForm
- Support optional array fields in
PathValueImpltype
- Preserve
Controller'sdefaultValuewithshouldUnregisterprop
- Improve get dirty fields logic
- Extra form values accessible via
formState
- Only execute trigger function when deps has a valid array
- Unregister previous field when switching conditional Controllers
- Use field name to update
isValidatingfields
- Sync two
defaultValuesafterresetwith newdefaultValues - Field name type conflict in nested
FieldErrors - Prevent
onBlurforreadOnlyfields - Do not override prototype of
dataincloneObject
- Reverted
watchreturn type change based ondefaultValue(caused regressions)
computeprop foruseWatchsubscription
- Subscribe with latest
defaultValues - Trigger watch callbacks only in response to value changes
- Track name with
setValuesubscription callbacks - Handle explicit
"multipart/form-data"encType in<Form />component - Remove React wildcard import to resolve ESM build issues
resetwithkeepFieldsRefoption to keep fields reference
- Remove
Setfrom clone object
- Support deep equality checking with circular references
- Issue with
formDatareference clone - Issue with
undefinedvalue for submit data useWatchobject variable param handling
FieldArrayPathByValuetype- Use
stringToPathto prevent errors at field names containing quotes
- Incorrect
formControlreturn fromuseForm - Initial
useFieldArrayfields population
- Focus form field for errors supplied via
errorsprop - Export
UseFormResetFieldOptionstype forresetFieldAPI - Root errors count in schema error lookup
- Type info for callback args in
subscribe - Proper types for
form.subscribe useControllerfocus function runtime issuesetValueskips values not in own properties (prevents infinite call stack with self-referencing proto)- Checkbox duplication handling in
useFieldArray modeandreValidateModereactivity- Default value being overwritten by
valuesprop regression
- Reactive
modeandreValidateModesupport isReadystate for subscription
- Regression on move/swap in
useFieldArrayinput update - Use
useIsomorphicLayoutEffectto address SSR warning
createFormControlandsubscribefunction- Infer resolver output types
- Allow components with
useControllerhook to be memoized - Track disabled fields and only omit data on submit
- Stable reference for
useWatchdefaultValue handleSubmitwith native events support
- Type inference for
useFormContext valuesanddefaultValueswork correctly withcreateFormControlanduseMemo- Prevent infinite render with condition check
- Valid state update with
onBlurmode disabledfield value not returned asundefinedin resolverrevalidateModeissue withuseFieldArrayvalidationuseControllerunregister issue with strict modesetErrorinuseEffectnot working insideFormProvidercontextuseControllerreturn props reference stability
useFormreturn methods will be memorized basedformStateupdate
- add support for
onBlurwith formStateisValid
validateFieldswill only trigger re-render for async validation
- added 'validateFields' to formState
const {
formState: { validateFields },
} = useForm();- add reactive
errorsprop atuseForm
useForm({
errors, // Server errors
});- added new
disabledprop foruseFormto disable the entire form
const App = () => {
const [disabled, setDisabled] = useState(false);
const { handleSubmit } = useForm({ disabled });
return (
<form
onSubmit={handleSubmit(async () => {
setDisabled(true);
await sleep(100);
setDisabled(false);
})}
/ >
);
}
resetapi withkeepIsSubmitSuccessfuloption, keep successfully submitted form state.
<Form
onSubmit={() => {
reset(formValues, {
keepIsSubmitSuccessful: true,
});
}}
/>- Controller
disabledprop
const [disabled, setDisabled] = useState(false);
useController({
disabled,
});- Trigger passed names to construct resolver options
- Add
exactoption for array name inuseWatch
- Update
isDirtywhen settingdisabledinregister
- Prevent
resetargument mutation
- Controller with type check on
onChange
- onChange: (...event: any[]) => void;
+ onChange: (event: ChangeEvent | FieldPathValue<TFieldValues, TName>) => void;- Include missing generic for
useFormContext
- export const useFormContext: <TFieldValues extends FieldValues, TransformedValues extends FieldValues | undefined = undefined>() => UseFormReturn<TFieldValues>;
+ export const useFormContext: <TFieldValues extends FieldValues, TContext = any, TransformedValues extends FieldValues | undefined = undefined>() => UseFormReturn<TFieldValues, TContext, TransformedValues>;- New
<Form />component
// Send post request with formData
<Form
action="/api"
control={control}
onSuccess={() => {
alert("Great");
}}
/>
// Send post request with json form data
<Form action="/api" encType="application/json" headers={{ accessToken: 'test' }}>
{errors.root?.server.type === 500 && 'Error message'}
{errors.root?.server.type === 400 && 'Error message'}
</Form>
// Send post request with formData with fetch
<Form
onSubmit={async ({ formData, data, formDataJson, event }) => {
await fetch("api", {
method: "post",
body: formData,
});
}}
/>- support
TransformedValueswithuseFormContext
useFormContext<FormValue, TransformedValues>()
- added
TTransformedValuestoFormProvider
FormProviderProps<TFieldValues, TContext, TTransformedValues>
- support global error type
const onSubmit = async () => {
setError('root.serverError', {
type: response.statusCode,
});
};
const onClick = () => {
setError('root.random', {
type: 'random',
});
};
return (
<>
{errors.root.serverError.type === 400 && <p>server response message</p>}
<p>{errors.root?.random?.message}</p>
</>
);- build in validation
validatesupport second argument for form values
// Making exported validate function isolated for validation
export function validateNumber(_: number, formValus: FormValues) {
return formValus.number1 + formValus.number2 === 3;
}
<input
type="number"
{...register('number1', {
validate: validateNumber,
valueAsNumber: true,
})}
/>;handleSubmitno longer catchonSubmitcallback error- Remove deprecated for
fieldState.invalid
useFormaddedvaluesprops
const values = await fetch('API');
useForm({
values, // will reset the form when values updates
// resetOptions: {
// keepDirtyValues: true
// }
});- new
isLoadingformState for asyncdefaultValues
const {
formState: { isLoading },
} = useForm();useFormsupport asyncdefaultValuesprops
const {
formState: { isLoading },
} = useForm({
defaultValues: fetch('API'),
// resetOptions: {
// keepDirtyValues: true
// }
});- async validation (or combined with sync) will always the take the latest validation result and abort the previous
- Conditional render
useFormStatewill trigger an extra re-render to reflect the currentformState
isValidformState is no longer only applicable withonChange,onTouched, andonBlurmode.
- support build-in validation with input type week and time
<input {...register("week", { min: "2022-W40" })} type="week" />
<input {...register("time", { min: "11:00" })} type="time" />- new formState
defaultValues
const { formState, watch } = useForm({
defaultValues: { name: 'test' },
});
const { defaultValues } = useFormState();
const name = watch('name');
return (
<div>
<p>Your name was {defaultValues.name}</p>
<p>Updated name is {name}</p>
</div>
);- defaultValues: complex object data contains prototype methods will not be cloned internally
- reset to support callback syntax
reset((formValues) => {
return {
...formValues,
partialData: 'onlyChangeThis',
};
});- new type
FieldPathByValuefield path by value generic implementation
function CustomFormComponent<
TFieldValues extends FieldValues,
Path extends FieldPathByValue<TFieldValues, Date>,
>({ control, name }: { control: Control<FieldValues>; name: Path }) {
const { field } = useController({
control,
name,
});
}
function App() {
const { control } = useForm<{
foo: Date;
baz: string;
}>();
return (
<form>
<CustomFormComponent control={control} name="foo" /> {/* no error */}
<CustomFormComponent control={control} name="baz" />{' '}
{/* throw an error since baz is string */}
</form>
);
}- form context support children prop type
<FormProvider {...methods}>
<div /> // β
<div /> // β
</FormProvider>- Build in validation support for
useFieldArraywithrulesprop
useFieldArray({
name: 'test',
rules: {
required: true,
minLength: 2,
maxLength: 10,
validate: (fieldArrayValues) => {
if (fieldArrayValues[2].title === 'test') {
return 'validate Error';
}
},
},
});
errors?.test?.root?.message; // access root level errors@hookform/resolversneeds to upgraded to version^2.9.3aboveuseFormContextdo always required to provide a generic type check for your form, without providing generic will now require developers to convert error messages toStringto pass the type check
useFormContext<FormValues>(); // β
correct usage by provide form type defination
const { formState } = useFormContext(); // if generic is missing
String(formState.errors?.input?.message); // will need to convert to string- Deprecate
NestedValueandUnpackNestedValuetype, will be removed in the next major version. Important: If you are using them, it may cause TS compile error, so please just remove the type usage.
type FormValues = {
- select: NestedValue<{
- nested: string
- }>
+ select: {
+ nested: string
+ }
}
type Data = UnpackNestedValue<FieldValues>formState'serrorsis now mapped/merged withFieldErrorUseFormHandleSubmithas removed unused function generic
UseFormRegisterReturnname type change fromstringtoTFieldName
- new:
resetoptional prop:keepDirtyValues
reset(
{
firstName: 'bill', // if firstName is dirty then the value will be retained
lastName: 'luo',
},
{ keepDirtyValues: true }, // keep any changed field
);useFieldArrayauto-correct field array errors on user action
const { append } = useFieldArray();
append({ data: '' }); // will auto correct existing field array errors if any- improve checkboxes value determine by defaultValues
useForm({
defaultValues: {
checkboxes: [], // register checkbox will be determine as array of checkboxes
},
});
register('checkboxes'); // will return array as value- tsconfig config change from es2017 to es2018
registerAPI optionsdepsnow support string
register('test', { deps: 'test' });- new option for
setFocusto select the entire field value
setFocus('fieldName', { shouldSelect: true });onTouchedmode will honorfocusoutevent
getFieldStateget individual field state
export default function App() {
const {
register,
getFieldState,
formState: { isDirty, isValid },
} = useForm({
mode: 'onChange',
defaultValues: {
firstName: '',
},
});
// you can invoke before render or within the render function
const fieldState = getFieldState('firstName');
return (
<form>
<input {...register('firstName', { required: true })} />
<p>{getFieldState('firstName').isDirty && 'dirty'}</p>
<p>{getFieldState('firstName').isTouched && 'touched'}</p>
<button
type="button"
onClick={() => console.log(getFieldState('firstName'))}
>
field state
</button>
</form>
);
}useControllerreturn prop:onChange,onBlurandrefwill be memorized withuseCallback
useFieldArraychangekeyNameis no longer required when field value containsid
const App = () => {
const { control, register, handleSubmit } = useForm<FormValues>({
defaultValues: {
test: [{ id: 'UUID5678', test: 'data' }], // id value will be retained
},
});
const { fields, append } = useFieldArray({
control,
name: 'test',
});
return (
<form>
{fields.map((field, index) => {
return <input key={field.id} {...register(`test.${index}.test`)} />;
})}
<button
type={'button'}
onClick={() => {
append({
id: 'UUID1234', // id value will be retained
test: '1234',
});
}}
>
append
</button>
</form>
);
};useFormStatewill no longer fire state update after hook unmountUseFormHandleSubmittype will infer formValues
- Browser native reset API will no longer be invoked when
resetprovided with value
const onSubmit = (data) => {};
React.useEffect(() => {
if (formState.isSubmitSuccessful) {
reset({ something: '' });
}
}, [formState, reset]);
handleSubmit(onSubmit);to
const onSubmit = (data) => {
setSubmittedData(data);
reset(data); // no longer need to have useEffect
};
handleSubmit(onSubmit);shouldUseNativeValidationwill pass down validation props both at client and server render
const { register } = useForm()
<input {...register('name', { required: true })} />
<input name="name" required /> // both client and server render- register
onChangewill share same logic withuseControllerfor non standard event payload
const { onChange } = register('test');
onChange('stringIsValid'); // this is only valid use case for JS- empty node in
formStatewill no longer gets unset
- new
exactprop foruseWatch - new
exactprop foruseFormState
useWatch({
name: 'test.test',
exact: true,
});
useFormState({
name: 'test.test',
exact: true,
});useWatchsubscription will occurred atuseEffectinstead beforerender
- new
resetFieldAPI
const { resetField } = useForm();
resetField('test');
resetField('test', {
keepError: true,
keepDirty: true,
keepTouched: true,
defaultValue: 'test1',
});useControllerwill return shallow clone value for the following data type on each rerender- object:
{... value} - array:
[...value]
- object:
- revert
FieldPathWithValue
- bring back
FieldPathWithValue - schema errors parent object look up
const validationSchema = object().shape({
questions: array().min(1, 'Array cannot be empty'),
});
// the above schema will be picked up by field array action
// the logic applies to group error object eg checkboxes
<button
type="button"
onClick={() => {
remove(questionIndex);
}}
>
Remove
</button>;- new type
FieldPathWithValueto improve generic components type support
type ExpectedType = { test: string };
const Generic = <FormValues extends FieldValues>({
name,
control,
}: {
name: FieldPathWithValue<FormValues, ExpectedType>;
control: Control<FormValues>;
}) => {
const {
field: { value, ...fieldProps },
} = useController<FormValues, ExpectedType>({
name,
control,
defaultValue: { test: 'value' },
});
return <input type="text" value={value.test} {...fieldProps} />;
};formStatesubscription no longer subscribed atuseEffectinstead the execution of each hook
registerallowed pass customonChangeandonBlur
<input
type="text"
{...register('test', {
onChange: (e) => {},
onBlur: (e) => {},
})}
/>useFieldArraynew methodreplace()
const { control } = useForm({
defaultValues: {
test: [{ value: 'lorem' }, { value: 'ipsum' }],
},
});
const { fields, replace } = useFieldArray({
control,
name: 'test',
});
const handleFullyReplacement = (): void => {
// remove old and set fully new values
replace([{ value: 'dolor' }, { value: 'sit' }, { value: 'amet' }]);
};- Improved to not map types defined with
interface.
import { useForm } from 'react-hook-form';
interface CustomValue {
custom: string;
}
type FormValues = {
fieldName: CustomValue;
};
const { formState: errors } = useForm<FormValues>({
defaultValues: { fieldName: { custom: 'value' } },
});registeradd dependent validation
const App = () => {
const { register, getValues } = useForm();
return (
<form>
<input
{...register('firstName', {
validate: (value) => {
return getValues('lastName') === value;
},
})}
/>
<input {...register('lastName', { deps: ['firstName'] })} /> // dependant
validation
</form>
);
};Trigger
- Trigger will enable object name trigger and field array name trigger
useFieldArray({ name: 'test' });
trigger('name'); // will trigger the whole field array to validateregister
- added a
disabledprop as an option to toggle input disable attribute - register will be able to seek input DOM reference through the
refcallback
register('test', { disabled: true }) // will set value to undefined and pass disabled down to the input attribute
<div {...register('test')}>
<input name="test" /> // this input will be registered
</div>useWatch
- added
disabledprop to toggle the state subscription.
useWatch({ disabled: true }); // you toggle the subscriptionuseFormState
- added
disabledprop to toggle the state subscription.
useFormState({ disabled: true }); // you toggle the subscriptionsetValue
- allow set value for non-registered inputs include nested object and array field.
<input {...register('test.0.firstName')} />
setValue('test', [{ firstName: 'bill' }, {firstName: 'kotaro}, {firstName: 'joris'}]) // this will works- new
useFormconfigdelayError
useForm({
delayError: 500, // delay error appear with 500ms
});updatemethod to update an field array inputs
const { update } = useFieldArray();
update(0, data); // update an individual field array nodedefaultValueis no longer a required prop for register input withuseFieldArray
- new config at
useFormto enabled native browser validation
const { register, handleSubmit } = useForm({
shouldUseNativeValidation: true,
});useControllerno longer access inputrefexceptfocusevent for focus management
setValuesupportshouldTouchto update formState touchFields
setValue('firstName', 'value', { shouldTouch: true });registernow acceptvalueas option
register('firstName', { value: 'value' });isValidwill initialise asfalse
shouldUnregister: falseshould not shallow merge or register absent input fields fromdefaultValues
triggersupport focus with error input
trigger('inputName', { shouldFocus: true });handleSubmitwillthrowerror within the onSubmit callback
useFormwillregistermissing inputs fromdefaultValues
const App = () => {
const { register, handleSubmit } = useForm({
defaultValues: {
test: { firstName: 'bill', lastName: 'luo' },
},
});
const onSubmit = (data) => {
// missing registered input will be included
console.log(data); // { test: { firstName: 'bill', lastName: 'luo' } }
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('test.firstName')} />
<button />
</form>
);
};isSubmitSuccessfulwill return false whenhandleSubmitcallback failed withErrororPromisereject.- unmounted input will no longer get validated even with
shouldUnregister: false
- new
nameprop foruseFormStateto subscribe to individual inputs.
useFormState({
name: 'inputName', // optional and can be array of inputs' name as well
});- set
shouldUnregistertotruewill not shallow mergedefaultValues
shouldUnregisterconfig to remove input value after unmount
// Global config (can't be overwrite)
useForm({
shouldUnregister: true // default to false
})
// Component/Hook level config (can not overwrites global config)
register('test', {
shouldUnregister: true // default to false
})
<Controller shouldUnregister={true} />
useController({ shouldUnregister: true })
useFieldArray({ shouldUnregister: true })registerwill retrieveonChange's target value when component'ref is not a valid input element.
- change type name from
RefCallbackHandlertoUseFormRegisterReturnfor register callback's return
useFieldArraywill produce an empty array[]when no field is presented.
setValuewith field array willregisterall inputs before rendering.
append,prependandinsertwillregisterdeeply nested inputs atuseFieldArray.
- typescript array index restriction removed.
append,prependandinsertwillregisterinputs during each action atuseFieldArray.
- change
ArrayKeytype tonumber | '${number}'
- Change
useController'smetaintofieldStateand includeformState, these change will be applied toControllertoo.
- const { field, meta } = useController({ control });
+ const { field, fieldState, formState } = useController({ control });- typescript array index support is changed to
49instead of99
- Breaking change:
valueAswill be run before the built-in validation andresolver
- <input {...register('test', { validate: (data: string) => {}, valueAsNumber: true })} />
+ <input {...register('test', { validate: (data: number) => {}, valueAsNumber: true })} />useWatchwill no longer requireddefaultValuefor field Array
- Breaking change: shallow merge defaultValues with result (#4074)
useForm({ defaultValues: { test: 'test' } });
getValues(); // v6 will return {}
getValues(); // v7 will return { test: 'test' }- Breaking change:
setError'sshouldFocusoption has been moved into the third argument.
- setError('test', { type: 'type', message: 'issue', shouldFocus: true })
+ setError('test', { type: 'type', message: 'issue' }, { shouldFocus: true })- Breaking change: type name changes:
- UseFormMethods
+ UseFormReturn
- UseFormOptions
+ UseFormProps
- UseFieldArrayMethods
+ UseFieldArrayReturn
- UseFieldArrayOptions
+ UseFieldArrayProps
- UseControllerMethods
+ UseControllerReturn
- UseControllerOptions
+ UseControllerProps
- ArrayField
+ FieldArray- fix
setValuewithControllerandresetwithuseFieldArrayissues: 4111 & 4108 (#4113)
- Breaking change:
setError'sshouldFocusoption has been moved to the third argument.
- setError('test', { type: 'type', message: 'issue', shouldFocus: true })
+ setError('test', { type: 'type', message: 'issue' }, { shouldFocus: true })- fix #4078 issue with watch + mode: onChange
- remove internal deep clone (#4088)
- remove transformToNestObject (#4089)
-
field name reference will be removed with
unregister(#4010) -
Breaking change: improve field array remove result and no longer remove field array value after unmount
const { remove } = useFieldArray({ name: 'test' })
remove();
getValues(); // V6: result form value {}
getValues(); // V7: result form value { test: [] }- change internal field names into
Set(#4015) - improve
onChangeperf with `resolver (#4017) - improve field array name look up perf (#4030)
- new custom hook
useFormState(#3740)
const { isDirty, errors } = useFormState();watchsupport can subscribe to the entire form with a callback
watch((data, { name, type }) => {
console.log('formValue', data);
console.log('name', name);
console.log('type', type);
});useControllerincludes newisValidatingstate (#3778)useControllerincludes newerrorstate (#3921)
const {
meta: { error, isValidating },
} = useController({ name: 'test' });- new
unregistersecond argument (#3964)
unregister('test', { keepDirty: true });- Resolver add
fieldbeing validated (#3881)
- resolver: (values: any, context?: object) => Promise<ResolverResult> | ResolverResult
+ resolver: (
+ values: any,
+ context?: object,
+ options: {
+ criteriaMode?: 'firstError' | 'all',
+ names?: string[],
+ fields: { [name]: field } // Support nested field
+ }
+ ) => Promise<ResolverResult> | ResolverResultuseFieldArrayaction can focus input by name and index
append(object, config: { shouldDirty: boolean, focusIndex: number, focusName: string })
insert(object, config: { shouldDirty: boolean, focusIndex: number, focusName: string })
prepend(object, config: { shouldDirty: boolean, focusIndex: number, focusName: string })-
Breaking change: No longer support IE 11 support
-
Breaking change:
registerhas been changed from register atrefto a function which needs to be spread as props.
- <input ref={register, { required: true }} name="test" />
+ <input {...register('name', { required: true })} />
+ <TextInput {...register('name', { required: true })} />- Breaking change:
namewith array will only support dot syntax instead of brackets.
- test[2].test
+ test.2.test
- Breaking change: remove
asprop atControllerand fix render prop consistency (#3732)
- <Controller render={props => <input {...props} />} />
+ <Controller render={({ field }) => <input {...field} />} />- Breaking change: remove
errorsalias (#3737)
- const { errors } = useForm();
+ const { formState: { errors } } = useForm();- Breaking change: improved
resetsecond argument (#3905)
- reset({}, { isDirty: true })
+ reset({}, { keepIsDirty: true })- Breaking change: change
touchedtotouchedFieldsfor consistency (#3923)
- const { formState: { touched } } = useForm();
+ const { formState: { touchedFields }} = useForm();- Breaking change:
triggerwill no longer return validation result.
- await trigger('test') // return true or false
+ trigger('test') // void-
remove
isSubmittingproxy (#4000) -
input
registerwill no longer be removed due to unmount, user will have to manually invokeunregister
useWatchinternal mechanism improvement (#3754)ControlleranduseControllerapplyuseFormStateinternally and improve performance (#3778)registertype support for input name (#3738)ControlleranduseCOntrollertype support for input name (#3738)useFieldArrayinternal logic and data structure improvement (#3858)- improve
useFieldArrayinternal fields update with subscription (#3943) - improve tests structure (#3916)
useWatchtype improvement (#3931)- improve type support for nested field array with
const(#3920) - improve
useFieldArrayinternal type structure (#3986) MutationObserverremoved fromuseForm
- radio input default selection will return
nullinstead of empty string'' valueAsNumberwith empty input will returnNaNinstead of0
setValuewithout shouldUnregister:false will no longer deep clone its value instead with shallow clone
- new formState
isValidating, this will set totrueduring validation.
const {
formState: { isValidating },
} = useForm();- When invoking
reset({ value })value will be shallow clone value object which you have supplied instead of deepClone.
// β avoid the following with deep nested default values
const defaultValues = { object: { deepNest: { file: new File() } } };
useForm({ defaultValues });
reset(defaultValues); // share the same reference
// β
it's safer with the following, as we only doing shallow clone with defaultValues
useForm({ deepNest: { file: new File() } });
reset({ deepNest: { file: new File() } });- New custom hook
useController: This custom hook is what powers Controller, and shares the same props and methods as Controller. It's useful to create reusable Controlled input, while Controller is the flexible option to drop into your page or form.
import React from 'react';
import { TextField } from '@material-ui/core';
import { useController } from 'react-hook-form';
function Input({ control, name }) {
const {
field: { ref, ...inputProps },
meta: { invalid, isTouched, isDirty },
} = useController({
name,
control,
rules: { required: true },
defaultValue: '',
});
return <TextField {...inputProps} inputRef={ref} />;
}useWatchwill retrieve the latest value fromreset(data)instead of returndefaultValue
useWatch({
name: 'test',
defaultValue: 'data', // this value will only show on the initial render
});- TS: name changed from
ValidationRulestoRegisterOptionsdue to valueAs functionality included asregisterfunction.
registerfunction with additional options to transform valuevalueAsDatevalueAsNumbersetValueAs
register({
valueAsNumber: true,
});
register({
valueAsNumber: true,
});
register({
setValueAs: (value) => value,
});defaultValuesis required to measureisDirty, keep a single source of truth to avoid multiple issues raised aroundisDirty- when
watchwithuseFieldArray,fieldsobject is no longer required as defaultValue
- watch('fieldArray', fields);
+ watch('fieldArray');Controllerwill have an extrarefprops to improve DX in terms of focus management.
<Controller
name="test"
render={(props) => {
return (
<input
value={props.value}
onChange={props.onChange}
ref={props.ref} // you can assign ref now without the use of `onFocus`
/>
);
}}
/>
// focus will work correct without the `onFocus` prop
<Controller name="test" as={<input />} />resolverwith group error object will no longer need withtriggerto show and clear error. This minor version made hook form look at parent error node to detect if there is any group error to show and hide.
const schema = z.object({
items: z.array(z.boolean()).refine((items) => items.some((item) => item)),
});
{
items.map((flag, index) => (
<input
type="checkbox"
defaultChecked={false}
// onChange={() => trigger("items")} now can be removed
ref={register}
name={`items.${index}`}
/>
));
}- with shouldUnregister set to false, empty Field Array will default [] as submission result.
const { handleSubmit } = useForm({
shouldUnregister: false,
});
useFieldArray({
name: 'test',
});
handleSubmit((data) => {
// shouldUnregister: false
// result: { data: {test: []} }
// shouldUnregister: true
// result: {}
});- when input unmounts
touchedanddirtyFieldswill no longer get removed fromformState(shouldUnregister: true).
- new formState
isSubmitSuccessfulto indicate successful submission setErrornow support focus on the actual input
setError('test', { message: 'This is required', shouldFocus: true });- with
shouldUnregister:falsedefaultValuesdata will be part of the submission data - with
shouldUnregister:falseconditional field is going to work withuseFieldArray setValuenow supportuseFieldArray
- setValue('test', 'data')
+ setValue('test', [{ test: '123' }]) // make it work for useFieldArray and target a field array key- remove
exactconfig at clearErrors
- clearErrors('test', { exact: false })
+ clearErrors('test') // does it automatically in the libclearErrorhave second option argument for clear errors which are exact or key name
register('test.firstName', { required: true });
register('test.lastName', { required: true });
clearErrors('test', { exact: false }); // will clear both errors from test.firstName and test.lastName
clearErrors('test.firstName'); // for clear single input error- all types from this lib has been exported. Important: only documented type: https://react-hook-form.com/ts will avoid breaking change.
errorsis also part offormStateobjectdisabledinput will not be part of the submission data by following the HTML standard
Controller'srenderprop will pass downnameprophandleSubmittake a second callback for errors callback- new mode
onTouchedwill only trigger validation after inputs are touched
registerno longer comparerefdifference with React Native
- IE 11 version will be required to install
@babel/runtime-corejs3as dependency at your own project
defaultValueis become required foruseFieldArrayat each input
- revert
getValueswill return default values before inputs registration
resolversupports both async and syncgetValueswill return default values before inputs registration
- export
ArrayFieldtype
- error message will support array of messages for specific type
- export type ValidateResult = Message | boolean | undefined;
+ export type ValidateResult = Message | Message[] | boolean | undefined;- Controller
onFocusworks with React Native - Controller stop producing
checkedprop by booleanvalue
- export
UseFormOptions,UseFieldArrayOptions,FieldError,FieldandModetype
- export
ValidationRulestype
- config for
shouldUnregisterwhich allow input to be persist even after unmount
useForm({
shouldUnregister: false, // unmount input state will be remained
});- auto focus with useFieldArray
append({}, (autoFocus = true));
prepend({}, (autoFocus = true));
insert({}, (autoFocus = true));- TS: NestedValue
import { useForm, NestedValue } from 'react-hook-form';
type FormValues = {
key1: string;
key2: number;
key3: NestedValue<{
key1: string;
key2: number;
}>;
key4: NestedValue<string[]>;
};
const { errors } = useForm<FormValues>();
errors?.key1?.message; // no type error
errors?.key2?.message; // no type error
errors?.key3?.message; // no type error
errors?.key4?.message; // no type erroruseWatch(new) subscribe to registered inputs.
<input name="test" ref={register} />;
function IsolateReRender() {
const { state } = useWatch({
name: 'test',
control,
defaultValue: 'default',
});
return <div>{state}</div>;
}getValues()support array of field names
getValues(['test', 'test1']); // { test: 'test', test1: 'test1' }useForm({ mode: 'all' })support all validation
-
rename
validationResolvertoresolver -
rename
validationContexttocontext -
rename
validateCriteriaModetocriteriaMode -
rename
triggerValidationtotrigger -
rename
clearErrortoclearErrors -
rename
FormContexttoFormProvider -
rename
dirtytoisDirty -
dirtyFieldschange type fromSettoObject -
Controller with render props API, and removed the following props:
- onChange
- onChangeName
- onBlur
- onBlurName
- valueName
-<Controller
- as={CustomInput}
- valueName="textValue"
- onChangeName="onTextChange"
- control={control}
- name="test"
-/>
+<Controller
+ render={({ onChange, onBlur, value }) => (
+ <CustomInput onTextChange={onChange} onBlur={onBlur} textValue={value} />
+ )}
+ control={control}
+ name="test"
+/>setErrorwill focus one error at a time and remove confusing set multiple errors, behavior change.- setError will persist an error if it's not part of the form, which requires manual remove with clearError
- setError error will be removed by validation rules, rules always take over errors
- setError('test', 'test', 'test')
+ setError('test', { type: 'test', message: 'bill'})setValuewill focus on input at a time
setValue('test', 'value', { shouldValidate: false, shouldDirty: false })- remove
validationSchemaand embrace validationresolver - remove
nestoption forwatch&getValues, so data return from both methods will be in FormValues shape.
-getValues({ nest: true }); // { test: { data: 'test' }}
-watch({ nest: true }); // { test: { data: 'test' }}
+getValues(); // { test: { data: 'test' }}
+watch(); // { test: { data: 'test' }}Controller: onChange will only evaluate payload as event like object. eg: react-select will no longer need the extraonChangemethod atController.
import { TextInput } from 'react-native';
-<Controller
- as={<TextInput style={{ borderWidth: 2, borderColor: 'black'}} />}
- name="text"
- control={args => ({
- value: args[0].nativeEvent.text,
- })}
- onChange={onChange}
-/>
+<Controller
+ as={<TextInput style={{ borderWidth: 2, borderColor: 'black'}} />}
+ name="text"
+ control={args => args[0].nativeEvent.text}
+ onChange={onChange}
+/>- improve module exports:
import { useForm } from 'react-hook-form';- nested
errorsobject and better typescript support
type form = {
yourDetail: {
firstName: string;
};
};
errors?.yourDetail?.firstName;- triggerValidation argument change from
Object,Object[]toString,String[]
triggerValidation('firstName');
triggerValidation(['firstName', 'lastName']);- watch support
{ nest: boolean }
watch(); // { 'test.firstName': 'bill' }
watch({ nest: true }); // { test: { firstName: 'bill' } }- improve custom
register
register('test', { required: true });- setError` support nested object
setError('yourDetail.firstName', 'test');
errors.yourDetails.firstName;handleSubmitno longer rerun array inputs containsundefinedornull
- move
RHFInputinto the main repo and rename it toController
<Controller control={control} name="test" />-
validationSchemaOption: hardly anyone want to use validation with abort early, or change the config. -
native validation: hardly anyone used this feature. https://react-hook-form.com/docs/#Browserbuiltinvalidation
React Hook Form return a new formState: Object which contain the following information
dirty: when user interactive any fieldstouched: what are the fields have interactedisSubmitted: whether the form have been triggered with submitting
const {
formState: { dirty, touched, isSubmitted },
} = useForm();- support
ref={register}instead of onlyref={register()}