Lesson 7

Validation Modes and Timing

React Hook Form doesn't validate fields at random. It follows a predictable timing model controlled by two options: mode and reValidateMode. Understanding these gives you fine-grained control over when users see errors — and how aggressively your form re-checks their input.

The mode Option

The mode option tells React Hook Form when to trigger validation for the first time. You set it in the useForm configuration:

const { register, handleSubmit, formState: { errors } } = useForm({
  mode: "onBlur",
});

This single option changes the entire feel of your form.

onSubmit (default)

useForm({ mode: "onSubmit" });

This is what you get if you don't specify a mode. Validation only runs when the user submits the form. Fields show no errors while the user is filling them in — they get all the feedback at once after clicking submit.

After that first submission fails, RHF switches to re-validating on change so errors clear as the user fixes them. This is the least intrusive mode. It works well for short forms where you don't want to distract the user while they're still typing.

onBlur

useForm({ mode: "onBlur" });

Validation triggers when a field loses focus — when the user tabs or clicks away from it. This gives feedback after the user finishes with a field but before they submit.

onBlur is a solid choice when you want per-field feedback without validating on every keystroke. It strikes a balance between being helpful and staying out of the way.

onChange

useForm({ mode: "onChange" });

Validation runs on every change event — every keystroke in a text input, every toggle of a checkbox. This is the most aggressive mode and gives the user instant feedback as they type.

The trade-off is performance. Every change triggers validation, which triggers a re-render if the error state changes. For simple forms this is fine. For large forms with expensive validation logic, it can feel sluggish. Use it when immediate feedback is more important than render efficiency — like a password strength indicator.

onTouched

useForm({ mode: "onTouched" });

This is a hybrid. The first validation for a field happens on blur — when the user leaves the field. After that, it switches to validating on every change.

The idea is that you don't want to show an error while someone is still typing their email address for the first time. But once they've left the field and an error appears, you want it to clear the moment they fix it. This gives you the best of both worlds and is a strong default for most forms.

all

useForm({ mode: "all" });

Validation fires on both blur and change simultaneously. This is the most comprehensive mode — errors show while typing and also when leaving a field. In practice, onTouched covers most of the same ground with fewer re-renders. Use all when you need validation to run at every possible moment, such as forms with critical real-time constraints.

reValidateMode

mode controls when validation happens the first time. reValidateMode controls what happens after an error is already showing. It accepts three values: "onChange" (default), "onBlur", or "onSubmit".

useForm({
  mode: "onBlur",
  reValidateMode: "onChange",
});

With this configuration, errors first appear when the user leaves a field. But once an error is visible, it re-validates on every change — so the error disappears the instant the user types a valid value. This is the default reValidateMode and it's the right choice for most cases.

If you set reValidateMode: "onBlur", errors only update when the user leaves the field again. This can feel unresponsive because the user types a fix but the error sticks around until they click away.

Setting reValidateMode: "onSubmit" means errors persist until the user submits again. This is unusual but could make sense for forms where you only want to validate in bulk.

Seeing It in Action

Here's a form using onBlur mode so you can see how the timing feels:

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

type FormData = {
  email: string;
  age: string;
};

function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>({
    mode: "onBlur",
    defaultValues: {
      email: "",
      age: "",
    },
  });

  const onSubmit = (data: FormData) => {
    console.log("Submitted:", data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          {...register("email", {
            required: "Email is required",
            pattern: {
              value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
              message: "Enter a valid email",
            },
          })}
        />
        {errors.email && (
          <p style={{ color: "red" }}>{errors.email.message}</p>
        )}
      </div>

      <div>
        <label htmlFor="age">Age</label>
        <input
          id="age"
          type="number"
          {...register("age", {
            required: "Age is required",
            min: { value: 18, message: "Must be at least 18" },
          })}
        />
        {errors.age && (
          <p style={{ color: "red" }}>{errors.age.message}</p>
        )}
      </div>

      <button type="submit">Sign Up</button>
    </form>
  );
}

export default SignupForm;

With mode: "onBlur", the user can type freely in the email field without seeing any errors. The moment they tab to the age field, the email validates. If it's invalid, the error appears immediately. Because reValidateMode defaults to "onChange", fixing the email clears the error as they type — they don't have to leave the field again.

Choosing the Right Mode

The decision comes down to UX versus performance. Here's a quick guide:

  • onSubmit — Best for short, simple forms. Minimal re-renders, but the user gets no feedback until they submit.
  • onBlur — Good balance. Errors appear after the user finishes with a field.
  • onChange — Instant feedback on every keystroke. Use for fields where real-time validation matters (passwords, usernames).
  • onTouched — The sweet spot for most forms. First validation on blur, then re-validates on change.
  • all — Maximum coverage. Rarely needed unless you have specific requirements.

For most forms, onTouched is the best default. It avoids showing errors while the user is still typing but clears them immediately once they start fixing the issue. Pair it with the default reValidateMode: "onChange" and your forms will feel responsive without unnecessary re-renders.