- 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/api/#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()}