| | |

React Hook Form 01 ๐Ÿ“‹ Why React Hook Form โ€” Uncontrolled inputs & minimal re-renders

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

React Hook Form exists because the controlled-input model, which is the default in React, does not scale. A controlled input stores its value in React state, and every keystroke triggers a state update, which triggers a re-render of the component that holds the state, and often of the entire form. For a login form with two fields, the re-render is invisible. For a dynamic form with fifty fields, a nested field array, and a validation schema, the re-render on every keystroke is a visible latency that the user feels as they type. React Hook Form was built to remove that cost by using the browser’s native input behaviorโ€”the uncontrolled inputโ€”and subscribing to the form state only where the UI actually reads it.

The library’s core idea is that the DOM already stores the input values, and React does not need to. The register function connects a native input to the form, and the input keeps its own value. The library reads the values when the form is submitted, or when a specific component subscribes to a specific field. The result is that typing does not re-render the form, the validation runs on the values that the DOM holds, and the formState objectโ€”the errors, the dirty fields, the submission stateโ€”re-renders only the components that read those specific properties . The official FAQ states the difference plainly: “React Hook Form uses refs and uncontrolled native inputs by default, while still supporting controlled components. This can reduce the amount of state and rendering work needed as a form grows” .

Key point: React Hook Form uses uncontrolled native inputs by default, so typing does not trigger a React re-render. The values live in the DOM, and the library reads them at submission or through a scoped subscription. The Controller component is the escape hatch for the controlled componentsโ€”the third-party selects, the date pickersโ€”that cannot expose a native ref. The trade-off is a different mental model: the values are not in React state, and the live-derived UI requires an explicit subscription.


Why React Hook Form exists

The re-render problem. A controlled input calls setState on every keystroke, and React re-renders the component tree that depends on that state. The re-render is cheap for a single field, but it compounds with the number of the fields, the complexity of the component tree, and the validation logic that runs during the render. The React Hook Form FAQ describes the uncontrolled model as the mechanism that “can reduce the amount of state and rendering work needed as a form grows” . The reduction is the primary reason the library exists.

The state-synchronization problem. A controlled form keeps the values in React state, and the DOM inputs are the reflection of that state. The two must be kept in sync, and the synchronization is the source of the bugs: the input that ignores the first keystroke because the value prop is set without the onChange, the field that switches from uncontrolled to controlled because the initial value is undefined, the form that resets the state but not the DOM. The uncontrolled model removes the synchronization by removing the React state. The DOM is the source of truth, and the library reads it .

The validation-timing problem. A controlled form validates on every keystroke because the state changes on every keystroke. The validation on every keystroke is expensive, and it produces the error messages while the user is still typing the first character. React Hook Form separates the validation timing from the value storage: the mode option controls when the validation runs, and the default is onSubmit. The mode: "onTouched" is the recommended setting for the forms that want the errors after the user leaves a field, and then update on every change . The separation is the mechanism that lets the validation be as aggressive or as relaxed as the UX requires.

The trade-off. The uncontrolled model is faster, but it is a different mental model. The values are not in React state, and a developer who is used to reading values.email in the render will not find it. The live-derived UIโ€”the preview that updates as the user types, the section that appears when a checkbox is checkedโ€”requires an explicit subscription with watch or useWatch, and the subscription re-introduces the controlled cost for the components that use it . The performance is the benefit, and the explicit subscription is the cost.

The bundle-size problem. The library ships at roughly 12 KB, with no dependencies, while Formik ships at roughly 44 KB with its dependencies . The size difference is not the primary reason to choose React Hook Form, but it is the secondary reason: a form library is loaded on every page that has a form, and the smaller bundle is the faster page. The zero-dependency design is the mechanism that keeps the size small, and the size is the mechanism that keeps the load time low.


a. The uncontrolled input and the register function

The register function is the connection between the native input and the form. It returns the name, the onChange, the onBlur, and the ref props, and the input spreads them. The ref is the mechanism that lets the library read the DOM value, and the onChange and the onBlur are the mechanisms that let the library run the validation and update the formState .

// The uncontrolled input with register
import { useForm } from "react-hook-form";

function LoginForm() {
  const { register, handleSubmit } = useForm({
    defaultValues: { email: "", password: "" },
    mode: "onTouched",
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      {/* The input spreads the register props. */}
      {/* The value is stored in the DOM, not in React state. */}
      <input {...register("email")} type="email" />
      <input {...register("password")} type="password" />
      <button type="submit">Log in</button>
    </form>
  );
}

The defaultValues option is the mechanism that sets the initial value. The value is read once on the first render, and the form compares the current values against it for the isDirty and the dirtyFields state . The mode: "onTouched" option is the mechanism that runs the validation when the user leaves the field, and then on every change after the first error .

The controlled component is the exception. A third-party select, a date picker, or a masked input that exposes a value and an onChange cannot use the register because it does not expose a native ref. The Controller component is the bridge, and the useController hook is the hook form of the same bridge .

// The controlled component with Controller
import { useForm, Controller } from "react-hook-form";
import DatePicker from "react-datepicker";

function BookingForm() {
  const { control, handleSubmit } = useForm();

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      {/* The Controller connects the controlled component. */}
      {/* The field is stored in the form, but the component owns the value. */}
      <Controller
        name="date"
        control={control}
        defaultValue={new Date()}
        render={({ field }) => (
          <DatePicker selected={field.value} onChange={field.onChange} />
        )}
      />
      <button type="submit">Book</button>
    </form>
  );
}

The Controller is the deliberate choice for the components that need it. The official documentation recommends the register as the default and the Controller as the exception, because the register is the fast path and the Controller re-introduces the controlled cost for the field it wraps .


b. The formState subscription and the minimal re-render

The formState object is the state that the form exposes: the errors, the isDirty, the isValid, the isSubmitting, the isSubmitSuccessful, the touchedFields, and the dirtyFields. The object is a proxy, and the library tracks which properties the component reads during the render. The component re-renders only when one of the properties it reads changes . A component that reads formState.errors.email does not re-render when the isDirty changes, because it did not read the isDirty.

// The narrow formState subscription
function EmailField() {
  const {
    register,
    formState: { errors },
  } = useForm();

  // The component reads only errors.email.
  // It re-renders only when errors.email changes.
  return (
    <div>
      <input {...register("email")} />
      {errors.email && <p>{errors.email.message}</p>}
    </div>
  );
}

The narrow subscription is the mechanism that keeps the re-renders proportional to the changes. A form with fifty fields, each in its own component, re-renders only the field that has the error, not the whole form. The formState is the state, and the proxy is the mechanism that tracks the reads .

The watch and the useWatch are the mechanisms for the live values. The watch subscribes to the value changes and re-renders the component that calls it, and the useWatch is the hook that scopes the subscription to the specific field. The getValues is the mechanism that reads the values without the subscription, and it is the one for the event handlers and the effects, not for the render .

// The scoped watch
import { useForm, useWatch } from "react-hook-form";

function ShippingForm() {
  const { register, control } = useForm();
  // The useWatch scopes the subscription to the country field.
  const country = useWatch({ control, name: "country" });

  return (
    <form>
      <input {...register("country")} />
      {/* Only this section re-renders when the country changes. */}
      {country === "US" && <input {...register("state")} />}
    </form>
  );
}

The useWatch is the mechanism that keeps the live-derived UI fast. The component that calls it re-renders on the field change, and the rest of the form does not. The official FAQ states the rule: “Read values at submit, not in render. Avoid watch('field') in the render path unless you specifically need to react to a value live… and when you do, scope it to the smallest component so only that subtree re-renders” .


Complete Example Session

// ============================================
// PART 1: THE BASIC FORM
// ============================================

import { useForm } from "react-hook-form";

type FormValues = {
  email: string;
  password: string;
};

function SignUpForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<FormValues>({
    defaultValues: { email: "", password: "" },
    mode: "onTouched",
  });

  const onSubmit = async (data: FormValues) => {
    // The data is read from the DOM at submission.
    await fetch("/api/sign-up", {
      method: "POST",
      body: JSON.stringify(data),
    });
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email")} type="email" />
      <input {...register("password")} type="password" />
      <button type="submit" disabled={isSubmitting}>
        Sign up
      </button>
    </form>
  );
}

// ============================================
// PART 2: THE REGISTER OPTIONS
// ============================================

// The register accepts the validation rules.
<input
  {...register("email", {
    required: "The email is required.",
    pattern: {
      value: /^\S+@\S+$/i,
      message: "The email is invalid.",
    },
  })}
  type="email"
/>

// ============================================
// PART 3: THE CONTROLLED COMPONENT
// ============================================

import { useForm, Controller } from "react-hook-form";
import Select from "react-select";

function CountryForm() {
  const { control, handleSubmit } = useForm();

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <Controller
        name="country"
        control={control}
        render={({ field }) => (
          <Select
            options={[{ value: "us", label: "United States" }]}
            onChange={(option) => field.onChange(option?.value)}
            value={{ value: field.value, label: field.value }}
          />
        )}
      />
    </form>
  );
}

// ============================================
// PART 4: THE FORMSTATE SUBSCRIPTION
// ============================================

function ErrorSummary() {
  const {
    formState: { errors, isDirty },
  } = useForm();

  // The component reads errors and isDirty.
  // It re-renders when either changes.
  return (
    <div>
      {isDirty && <p>The form has unsaved changes.</p>}
      {errors.email && <p>{errors.email.message}</p>}
    </div>
  );
}

// ============================================
// PART 5: THE SCOPED WATCH
// ============================================

import { useForm, useWatch } from "react-hook-form";

function ConditionalForm() {
  const { register, control } = useForm();
  const hasCompany = useWatch({ control, name: "hasCompany" });

  return (
    <form>
      <input type="checkbox" {...register("hasCompany")} />
      {hasCompany && <input {...register("companyName")} />}
    </form>
  );
}

// ============================================
// PART 6: THE FORM ARRAY
// ============================================

import { useForm, useFieldArray } from "react-hook-form";

function ItemsForm() {
  const { register, control, handleSubmit } = useForm({
    defaultValues: { items: [{ name: "" }] },
  });
  const { fields, append, remove } = useFieldArray({
    control,
    name: "items",
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input {...register(`items.${index}.name`)} />
          <button type="button" onClick={() => remove(index)}>
            Remove
          </button>
        </div>
      ))}
      <button type="button" onClick={() => append({ name: "" })}>
        Add
      </button>
    </form>
  );
}

// ============================================
// PART 7: THE RESET
// ============================================

function ResetForm() {
  const { register, reset, handleSubmit } = useForm({
    defaultValues: { name: "" },
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register("name")} />
      <button type="button" onClick={() => reset()}>
        Reset
      </button>
    </form>
  );
}

// ============================================
// PART 8: THE SCHEMA RESOLVER
// ============================================

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

function SchemaForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register("email")} />
      {errors.email && <p>{errors.email.message}</p>}
      <input {...register("password")} type="password" />
      {errors.password && <p>{errors.password.message}</p>}
    </form>
  );
}

// ============================================
// PART 9: THE DEFAULT VALUES FROM THE API
// ============================================

// The values prop is the mechanism for the async data.
function EditForm({ user }) {
  const { register, handleSubmit } = useForm({
    // The values prop updates the form when the data changes.
    values: user,
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register("name")} />
    </form>
  );
}

// ============================================
// PART 10: THE CHECKLIST
// ============================================

// [ ] The register is used for the native inputs.
// [ ] The Controller is used for the controlled components.
// [ ] The mode is set to "onTouched" for the late validation.
// [ ] The formState is destructured narrowly.
// [ ] The useWatch is used for the live-derived UI.
// [ ] The defaultValues is set for every field.
// [ ] The values prop is used for the async data.

The ten parts covered the basic form, the register options, the controlled component, the formState subscription, the scoped watch, the form array, the reset, the schema resolver, the async data, and the checklist. The through-line is that the uncontrolled inputs keep the typing off the React render path, and the scoped subscriptions keep the re-renders proportional to the changes.


Quick Reference

The core API

APIPurpose
useFormThe form hook, the entry point
registerConnects the native input
handleSubmitWraps the submit handler
formStateThe errors, the dirty state, the submission state
ControllerConnects the controlled component
useWatchThe scoped live value subscription
useFieldArrayThe dynamic field list

The validation modes

ModeWhen the validation runs
onSubmitThe default, on submission
onTouchedOn blur, then on change
onChangeOn every change
allOn blur and on change

The state subscription

MechanismRe-renders
registerNo re-render on typing
formState.errorsOnly when the error changes
watchOn every value change
useWatchOn the scoped value change

The library comparison

LibraryState modelBundle size
React Hook FormUncontrolled~12 KB
FormikControlled~44 KB
React Final FormSubscriptionโ€”

Best Practices

โœ… Do This:

// Use register for the native inputs
<input {...register("email")} />                                  // โœ…

// Use mode: "onTouched" for the late validation
useForm({ mode: "onTouched" });                                   // โœ…

// Use useWatch for the live-derived UI
const value = useWatch({ control, name: "country" });             // โœ…

// Set the defaultValues for every field
useForm({ defaultValues: { email: "" } });                        // โœ…

โŒ Don’t Do This:

// Wrap every field in Controller
<Controller name="email" control={control} render={...} />        // โŒ

// Mirror the RHF values into useState
const [email, setEmail] = useState(); watch("email");             // โŒ

// Read the formState in the event handler
const { formState: { isValid } } = useForm(); onClick={() => isValid} // โŒ

Common Pitfalls

PitfallWhy It HappensFix
The field is empty after the API loadsThe defaultValues is read onceUse the values prop
The controlled input ignores the first keystrokeThe value prop is set without the onChangeUse the register or the Controller
The isValid is always falseThe formState is read only in the handlerRead it during the render
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 register

<input {...register("email")} />

2. The register with the validation

<input {...register("email", { required: "Required" })} />

3. The Controller

<Controller name="date" control={control} render={({ field }) => <DatePicker {...field} />} />

4. The formState errors

const { formState: { errors } } = useForm();

5. The useWatch

const country = useWatch({ control, name: "country" });

6. The useFieldArray

const { fields, append } = useFieldArray({ control, name: "items" });

7. The reset

reset({ email: "" });

8. The zodResolver

useForm({ resolver: zodResolver(schema) });

9. The values prop

useForm({ values: user });

10. The mode

useForm({ mode: "onTouched" });

Visual

The Uncontrolled vs Controlled

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE UNCONTROLLED INPUT                                      โ”‚
โ”‚                                                              โ”‚
โ”‚  The user types                                              โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ The DOM updates     โ”‚  No React render                   โ”‚
โ”‚  โ”‚ the input value     โ”‚                                    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚             โ”‚                                                โ”‚
โ”‚             โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ The library reads   โ”‚  At submission or on the           โ”‚
โ”‚  โ”‚ the value           โ”‚  scoped subscription               โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  The controlled input would call setState on every keystrokeโ”‚
โ”‚  and re-render the form.                                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The formState Subscription

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  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 Library Comparison

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  REACT HOOK FORM VS FORMIK                                   โ”‚
โ”‚                                                              โ”‚
โ”‚  React Hook Form              Formik                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”‚
โ”‚  โ”‚ Uncontrolled        โ”‚     โ”‚ Controlled          โ”‚        โ”‚
โ”‚  โ”‚ No re-render on     โ”‚     โ”‚ Re-render on every  โ”‚        โ”‚
โ”‚  โ”‚ typing              โ”‚     โ”‚ keystroke           โ”‚        โ”‚
โ”‚  โ”‚ ~12 KB              โ”‚     โ”‚ ~44 KB              โ”‚        โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ”‚
โ”‚                                                              โ”‚
โ”‚  The uncontrolled model is the performance advantage.        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Core modelUncontrolled inputs by default
registerConnects the native input, no re-render on typing
ControllerThe escape hatch for the controlled components
formStateThe proxy that tracks the reads
useWatchThe scoped live value subscription
modeThe validation timing, onTouched recommended
Bundle size~12 KB, no dependencies
ComparisonFormik is controlled and ~44 KB

Key takeaways:

  • The uncontrolled inputs are the performance mechanism. The register connects the native input, and the DOM stores the value. Typing does not trigger a React re-render, which is the reason the large forms stay responsive .
  • The Controller is the exception, not the default. The controlled componentsโ€”the third-party selects, the date pickers, the masked inputsโ€”need the Controller, and wrapping every field in it throws away the performance advantage .
  • The formState is a proxy that tracks the reads. A component re-renders only when the properties it reads change. The narrow destructuring is the mechanism that keeps the re-renders proportional to the changes .
  • The useWatch is the scoped subscription for the live-derived UI. The watch in the render path re-renders the whole form, and the useWatch scopes the re-render to the component that reads the value .
  • The mode separates the validation timing from the value storage. The default is onSubmit, and the onTouched is the recommended setting for the errors after the user leaves a field .

Remember: React Hook Form exists because the controlled input does not scale. The register connects the native input, and the DOM stores the value. The formState is a proxy that re-renders only the components that read the changed properties. The Controller is the escape hatch for the controlled components. The useWatch is the scoped subscription for the live-derived UI. The trade-off is the different mental model: the values are not in React state, and the live-derived UI requires the explicit subscription.

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!