React Hook Form 03 ๐ The useForm Hook โ Core setup and return values
The useForm hook is the entry point for every React Hook Form form. It creates the form’s internal state, wires the validation, and returns the methods and the state that the rest of the component uses. The hook is called once per form, and the returned object is the toolbox: the register connects the inputs, the handleSubmit wraps the submission, the formState exposes the errors and the status, and the control connects the controlled components and the field arrays. The options passed to the hook are the form’s configuration, and the defaults are chosen so that a form with no options is still functional.
The return values are not a flat set of independent properties. The formState is a proxy that tracks which properties the component reads during the render, and the component re-renders only when one of the properties it reads changes . This is the mechanism that makes the minimal re-render possible: a component that reads formState.errors does not re-render when formState.isDirty changes. The register returns the props that connect the native input, and the handleSubmit returns a function that validates the form and calls the submit handler only if the validation passes .
Key point: useForm returns the methods (register, handleSubmit, watch, setValue, getValues, reset, trigger, control) and the formState object. The formState is a proxy that re-renders only the components that read the changed properties. The register connects the native inputs, and the handleSubmit validates before it calls the submit handler. The defaultValues option is required for the isDirty tracking and the controlled-component behavior.
Why the useForm hook is the core
The single-call problem. A form needs the input registration, the validation, the submission handling, the error state, and the reset. A library that exposes each of these as a separate hook would require the developer to wire them together. The useForm hook returns all of them from a single call, and the call is the form’s initialization. The returned object is the form’s API, and the rest of the component uses it .
The options problem. The form’s behavior is determined by the options. The defaultValues sets the initial values and the baseline for the isDirty comparison. The mode controls when the validation runs. The resolver connects the schema validation. The shouldUnregister controls whether the unmounted fields keep their values . The options are the configuration, and the defaults are chosen for the common case: the mode is onSubmit, the shouldUnregister is false, and the validation runs only after the first submission.
The return-value problem. The returned object is larger than the component usually needs. The component destructures the methods it uses: the register and the handleSubmit for the basic form, the formState.errors for the error display, the isSubmitting for the button state. The formState is the proxy, and the destructuring is the mechanism that tells the proxy which properties to track . A component that destructures the formState in the render subscribes to the properties it destructures, and the subscription is the mechanism that keeps the re-renders proportional to the changes.
The trade-off. The useForm hook is the single entry point, and the single entry point means the component is coupled to the hook. A form that needs a different state management approach cannot use the hook. The trade-off is the simplicity against the flexibility, and the hook is the right choice for the forms that fit the model.
a. The core options and the initialization
The useForm hook accepts an options object, and the options are all optional. The defaultValues is the most important, because it sets the initial values and the baseline for the isDirty and the dirtyFields comparison . The mode controls when the validation runs, and the default is onSubmit. The resolver connects the schema validation, and the shouldUnregister controls the unmount behavior.
// The core options
import { useForm } from "react-hook-form";
type LoginFormValues = {
email: string;
password: string;
};
function LoginForm() {
const form = useForm<LoginFormValues>({
// The initial values and the dirty baseline.
defaultValues: { email: "", password: "" },
// The validation runs on submit by default.
mode: "onTouched",
// The schema validation, if a resolver is used.
// resolver: zodResolver(schema),
// The unmounted fields keep their values by default.
shouldUnregister: false,
});
return <form>{/* ... */}</form>;
}
The defaultValues is the baseline for the isDirty comparison. The library compares the current values against the defaultValues, and a field is dirty when the current value differs from the default . The option is also the mechanism that prevents the uncontrolled-to-controlled warning: an input with no initial value starts as uncontrolled and becomes controlled when the value changes, and the defaultValues gives the input an initial value.
The mode is the validation timing. The onSubmit is the default, and the validation runs only when the form is submitted. The onTouched runs the validation when the user leaves a field, and then on every change after the first error. The onChange runs the validation on every change, and the all runs it on both the blur and the change . The choice is the UX decision: the onSubmit is the least intrusive, and the onChange is the most immediate.
The resolver is the schema validation. The zodResolver, the yupResolver, and the valibotResolver are the functions that connect the schema to the form. The resolver runs on the complete form data, and it returns the typed values or the validation errors . The validate option on the register is the alternative, and the two are mutually exclusive: the resolver runs if both are set .
b. The return values and the formState proxy
The useForm hook returns an object with the methods and the state. The methods are the register, the handleSubmit, the watch, the setValue, the getValues, the reset, the trigger, the control, the setError, the clearErrors, the setFocus, and the unregister . The state is the formState, and the formState is the object that holds the errors, the dirty state, the submission state, and the validation state.
// The destructured return values
const {
register, // Connects the native inputs.
handleSubmit, // Wraps the submit handler.
formState: {
errors, // The field errors.
isSubmitting, // True while the submit is pending.
isDirty, // True when a field differs from the default.
isValid, // True when there are no errors.
},
reset, // Resets the form.
setValue, // Sets a field value programmatically.
getValues, // Reads the values without a subscription.
control, // Connects the controlled components.
} = useForm<LoginFormValues>();
The formState is the proxy. The component re-renders only when the properties it reads during the render change . A component that destructures the errors re-renders when the errors change, and it does not re-render when the isSubmitting changes. The narrow destructuring is the mechanism that keeps the re-renders proportional to the changes.
The isDirty is the state that requires the defaultValues. The library compares the current values against the defaultValues, and a field is dirty when the value differs . The dirtyFields is the per-field version, and the isDirty is the form-level version. The isValid is the state that is computed from the errors, and the isSubmitting is the state that is true while the onSubmit promise is pending .
The handleSubmit is the function that validates before it submits. It calls the onSubmit handler with the typed data if the validation passes, and it calls the onInvalid handler with the errors if the validation fails . The two handlers are the mechanism that separates the success path from the failure path.
// The handleSubmit with the valid and invalid handlers
const onSubmit = (data: LoginFormValues) => {
console.log("Valid:", data);
};
const onInvalid = (errors: FieldErrors<LoginFormValues>) => {
console.log("Invalid:", errors);
};
<form onSubmit={handleSubmit(onSubmit, onInvalid)}>
{/* ... */}
</form>
Complete Example Session
// ============================================
// PART 1: THE BASIC USE FORM
// ============================================
import { useForm } from "react-hook-form";
type LoginFormValues = {
email: string;
password: string;
};
function LoginForm() {
const { register, handleSubmit } = useForm<LoginFormValues>({
defaultValues: { email: "", password: "" },
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("email")} />
<input {...register("password")} type="password" />
<button type="submit">Log in</button>
</form>
);
}
// ============================================
// PART 2: THE MODE OPTION
// ============================================
// The mode controls when the validation runs.
useForm({
mode: "onTouched", // The validation runs on blur, then on change.
});
// The default is "onSubmit".
// The "onTouched" is the recommended setting for the late errors.
// ============================================
// PART 3: THE DEFAULT VALUES
// ============================================
// The defaultValues is the baseline for the isDirty comparison.
useForm({
defaultValues: { email: "", password: "" },
});
// A missing field is a compile error with the generic.
// A missing field makes the isDirty comparison unreliable.
// ============================================
// PART 4: THE FORMSTATE PROXY
// ============================================
function ErrorDisplay() {
const {
formState: { errors },
} = useForm();
// The component reads only errors.
// It re-renders only when errors changes.
return <>{errors.email && <p>{errors.email.message}</p>}</>;
}
// ============================================
// PART 5: THE SUBMIT STATE
// ============================================
function SubmitButton() {
const {
formState: { isSubmitting },
} = useForm();
// The isSubmitting is true while the onSubmit promise is pending.
return <button disabled={isSubmitting}>Submit</button>;
}
// ============================================
// PART 6: THE IS DIRTY STATE
// ============================================
function SaveButton() {
const {
formState: { isDirty },
} = useForm();
// The isDirty is true when a field differs from the default.
return <button disabled={!isDirty}>Save</button>;
}
// ============================================
// PART 7: THE HANDLE SUBMIT HANDLERS
// ============================================
const onSubmit = (data: LoginFormValues) => {
console.log("Valid:", data);
};
const onInvalid = (errors: FieldErrors<LoginFormValues>) => {
console.log("Invalid:", errors);
};
<form onSubmit={handleSubmit(onSubmit, onInvalid)}>
{/* ... */}
</form>
// ============================================
// PART 8: THE RESET
// ============================================
function ResetButton() {
const { reset } = useForm();
// The reset restores the defaultValues and clears the errors.
return <button type="button" onClick={() => reset()}>Reset</button>;
}
// ============================================
// PART 9: THE CONTROL
// ============================================
import { useForm, Controller } from "react-hook-form";
function ControlledField() {
const { control } = useForm();
// The control connects the Controller and the useFieldArray.
return (
<Controller
name="date"
control={control}
render={({ field }) => <DatePicker {...field} />}
/>
);
}
// ============================================
// PART 10: THE CHECKLIST
// ============================================
// [ ] The useForm has the generic.
// [ ] The defaultValues is set for every field.
// [ ] The mode is set to "onTouched" for the late errors.
// [ ] The formState is destructured narrowly.
// [ ] The handleSubmit has the onInvalid handler.
// [ ] The control is used for the Controller and the useFieldArray.
The ten parts covered the basic useForm, the mode option, the default values, the formState proxy, the submit state, the isDirty state, the handleSubmit handlers, the reset, the control, and the checklist. The through-line is that the hook returns the methods and the proxy state, and the narrow destructuring is the mechanism that keeps the re-renders proportional.
Quick Reference
The useForm options
| Option | Type | Default | Purpose |
|---|---|---|---|
defaultValues | object | () => object | Promise<object> | โ | The initial values and the dirty baseline |
mode | 'onSubmit' | 'onChange' | 'onBlur' | 'all' | 'onSubmit' | The validation timing |
reValidateMode | 'onChange' | 'onBlur' | 'onChange' | The re-validation timing after the first error |
resolver | Resolver | โ | The schema validation |
shouldUnregister | boolean | false | The unmount behavior |
criteriaMode | 'firstError' | 'all' | 'firstError' | The error detail level |
The return values
| Return | Type | Purpose |
|---|---|---|
register | function | Connects the native inputs |
handleSubmit | function | Wraps the submit handler |
formState | object | The errors, the dirty state, the submission state |
watch | function | The live value subscription |
setValue | function | Sets a field value |
getValues | function | Reads the values without a subscription |
reset | function | Resets the form |
trigger | function | Runs the validation manually |
control | object | Connects the Controller and the useFieldArray |
The formState properties
| Property | Type | Purpose |
|---|---|---|
errors | object | The field errors |
isDirty | boolean | True when a field differs from the default |
dirtyFields | object | The per-field dirty state |
isSubmitting | boolean | True while the submit is pending |
isValid | boolean | True when there are no errors |
isSubmitSuccessful | boolean | True when the last submit succeeded |
Best Practices
โ Do This:
// Set the defaultValues for every field
useForm<FormValues>({ defaultValues: { email: "", password: "" } }); // โ
// Use the mode: "onTouched" for the late errors
useForm({ mode: "onTouched" }); // โ
// Destructure the formState narrowly
const { formState: { errors } } = useForm(); // โ
// Use the handleSubmit with the onInvalid handler
handleSubmit(onSubmit, onInvalid) // โ
โ Don’t Do This:
// Omit the defaultValues
useForm() // the isDirty is unreliable // โ
// Read the formState only in the handler
const { formState } = useForm(); onClick={() => formState.isValid} // โ
// Use the watch in the render for the whole form
const values = watch(); // the whole form re-renders // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
The isDirty is always true | The defaultValues is missing | Set the defaultValues |
| The errors do not appear until the submit | The mode is onSubmit | Set the mode: "onTouched" |
The isValid is always false in the handler | The formState is read only in the handler | Read it during the render |
| The field is empty after the API loads | The defaultValues is read once | Use the values prop or the reset |
| The form re-renders on every keystroke | The watch is used in the render | Use the useWatch scoped to the component |
Real-World Examples
1. The basic useForm
const { register, handleSubmit } = useForm<FormValues>();
2. The defaultValues
useForm({ defaultValues: { email: "", password: "" } });
3. The mode
useForm({ mode: "onTouched" });
4. The resolver
useForm({ resolver: zodResolver(schema) });
5. The formState errors
const { formState: { errors } } = useForm();
6. The isSubmitting
const { formState: { isSubmitting } } = useForm();
7. The isDirty
const { formState: { isDirty } } = useForm();
8. The handleSubmit
handleSubmit(onSubmit, onInvalid)
9. The reset
const { reset } = useForm();
10. The control
const { control } = useForm();
Visual
The useForm Return Values
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE useForm RETURN OBJECT โ
โ โ
โ useForm() โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ register โ Connects the native inputs โ
โ โ handleSubmit โ Wraps the submit handler โ
โ โ formState โ The proxy state โ
โ โ watch โ The live value subscription โ
โ โ setValue โ Sets a field value โ
โ โ getValues โ Reads the values โ
โ โ reset โ Resets the form โ
โ โ control โ Connects the Controller โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The formState Proxy
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE NARROW SUBSCRIPTION โ
โ โ
โ formState โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ errors โ Read by the component โ
โ โ isDirty โ Not read, no re-render โ
โ โ isSubmitting โ Not read, no re-render โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The component re- โ Only when errors changes โ
โ โ renders โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The handleSubmit Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE handleSubmit FLOW โ
โ โ
โ The form is submitted โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ handleSubmit โ The validation runs โ
โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ
โ โโโโโโโโดโโโโโโโ โ
โ โผ โผ โ
โ โโโโโโโโโโ โโโโโโโโโโ โ
โ โ Valid โ โInvalid โ โ
โ โ โ โ โ โ
โ โ onSubmitโ โonInvalidโ โ
โ โโโโโโโโโโ โโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Hook | useForm<FormValues>(options) |
defaultValues | The initial values and the dirty baseline |
mode | The validation timing, onSubmit default |
resolver | The schema validation |
formState | The proxy that re-renders only the read properties |
register | Connects the native inputs |
handleSubmit | Validates before it calls the handler |
control | Connects the Controller and the useFieldArray |
Key takeaways:
useFormis the single entry point. It returns the methods and the state from one call. Theregisterconnects the inputs, thehandleSubmitvalidates before it submits, and theformStateexposes the errors and the status .- The
defaultValuesis required for theisDirtytracking. The library compares the current values against thedefaultValues, and a missing field makes the comparison unreliable . The option is also the mechanism that prevents the uncontrolled-to-controlled warning. - The
modecontrols the validation timing. The default isonSubmit, and theonTouchedis the recommended setting for the errors after the user leaves a field . The choice is the UX decision. - The
formStateis a proxy. The component re-renders only when the properties it reads during the render change . The narrow destructuring is the mechanism that keeps the re-renders proportional to the changes. - The
handleSubmitseparates the valid and invalid paths. TheonSubmithandler receives the typed data, and theonInvalidhandler receives the errors . The two handlers are the mechanism that handles the success and the failure.
Remember: The useForm hook is the form’s initialization. It returns the methods and the proxy state. The defaultValues is the dirty baseline, the mode is the validation timing, and the resolver is the schema validation. The formState is the proxy that re-renders only the read properties. The register connects the inputs, and the handleSubmit validates before it submits. The hook is the core, and the options are the configuration.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!