| | |

React Hook Form 03 ๐Ÿ“‹ The useForm Hook โ€” Core setup and return values

โฑ๏ธ Estimated read: 13 min | |

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

OptionTypeDefaultPurpose
defaultValuesobject | () => 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
resolverResolverโ€”The schema validation
shouldUnregisterbooleanfalseThe unmount behavior
criteriaMode'firstError' | 'all''firstError'The error detail level

The return values

ReturnTypePurpose
registerfunctionConnects the native inputs
handleSubmitfunctionWraps the submit handler
formStateobjectThe errors, the dirty state, the submission state
watchfunctionThe live value subscription
setValuefunctionSets a field value
getValuesfunctionReads the values without a subscription
resetfunctionResets the form
triggerfunctionRuns the validation manually
controlobjectConnects the Controller and the useFieldArray

The formState properties

PropertyTypePurpose
errorsobjectThe field errors
isDirtybooleanTrue when a field differs from the default
dirtyFieldsobjectThe per-field dirty state
isSubmittingbooleanTrue while the submit is pending
isValidbooleanTrue when there are no errors
isSubmitSuccessfulbooleanTrue 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

PitfallWhy It HappensFix
The isDirty is always trueThe defaultValues is missingSet the defaultValues
The errors do not appear until the submitThe mode is onSubmitSet the mode: "onTouched"
The isValid is always false in the handlerThe formState is read only in the handlerRead it during the render
The field is empty after the API loadsThe defaultValues is read onceUse the values prop or the reset
The form re-renders on every keystrokeThe watch is used in the renderUse 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

ItemValue
HookuseForm<FormValues>(options)
defaultValuesThe initial values and the dirty baseline
modeThe validation timing, onSubmit default
resolverThe schema validation
formStateThe proxy that re-renders only the read properties
registerConnects the native inputs
handleSubmitValidates before it calls the handler
controlConnects the Controller and the useFieldArray

Key takeaways:

  • useForm is the single entry point. It returns the methods and the state from one call. The register connects the inputs, the handleSubmit validates before it submits, and the formState exposes the errors and the status .
  • The defaultValues is required for the isDirty tracking. The library compares the current values against the defaultValues, and a missing field makes the comparison unreliable . The option is also the mechanism that prevents the uncontrolled-to-controlled warning.
  • The mode controls the validation timing. The default is onSubmit, and the onTouched is the recommended setting for the errors after the user leaves a field . The choice is the UX decision.
  • The formState is 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 handleSubmit separates the valid and invalid paths. The onSubmit handler receives the typed data, and the onInvalid handler 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.

Was this helpful?


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!