Home/Learn/React/React Hook Form

React Hook Form

Intermediate
Forms

The library nearly every React job uses for forms. It keeps inputs uncontrolled for speed, wires validation to a schema, and removes the boilerplate you would otherwise write for every field.

Overview

Hand-rolling a form works for two fields. At ten, with per-field touched state, error state, submit state and validation timing, it becomes a large amount of repetitive code that is easy to get subtly wrong. React Hook Form registers each input directly with the DOM, so typing does not re-render the form, and exposes exactly the state a form needs. Knowing it is close to expected for a React role, and the API is small enough to learn in an afternoon.

The Basic Form

register, handleSubmit, and formState — that is most of the library.

register, handleSubmit, formState
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'

function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting, isDirty, isValid },
    reset,
    setError,
  } = useForm({
    resolver: zodResolver(SignupSchema),     // the schema from the last concept
    defaultValues: { name: '', email: '' },
    mode: 'onBlur',                          // the timing convention
  })

  async function onSubmit(values) {          // only runs if validation passed
    await signup(values)
    reset()
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} aria-invalid={!!errors.email} />
      {errors.email && <p role="alert">{errors.email.message}</p>}

      <button disabled={isSubmitting || !isDirty}>
        {isSubmitting ? 'Creating…' : 'Create account'}
      </button>
    </form>
  )
}

// Typing re-renders nothing. Only error state changes cause renders.

Server Errors and Async Checks

Bringing backend failures into the same error display.

setError for server failures
async function onSubmit(values) {
  try {
    await signup(values)
  } catch (err) {
    if (err.status === 422) {
      for (const [field, message] of Object.entries(err.fieldErrors)) {
        setError(field, { type: 'server', message })    // same UI as client errors
      }
      return
    }
    setError('root', { message: 'Something went wrong. Please try again.' })
  }
}
{errors.root && <p role="alert">{errors.root.message}</p>}

// Async validation for uniqueness, debounced by the resolver's timing
email: z.string().email().refine(
  async (v) => !(await emailTaken(v)),
  'That email is already registered',
)

// Watching a field to drive conditional UI
const plan = watch('plan')
{plan === 'team' && <input {...register('seats')} />}
// watch re-renders on change — use it deliberately, not everywhere.

Custom and Third-Party Inputs

Controller is how a component that does not expose a DOM ref joins in.

Controller and useFieldArray
import { Controller } from 'react-hook-form'

// A date picker, a rich select, an editor — anything with its own
// value/onChange API rather than a plain input
<Controller
  name="deadline"
  control={control}
  render={({ field, fieldState }) => (
    <DatePicker
      value={field.value}
      onChange={field.onChange}
      onBlur={field.onBlur}
      error={fieldState.error?.message}
    />
  )}
/>

// Dynamic lists of fields
const { fields, append, remove } = useFieldArray({ control, name: 'items' })
{fields.map((f, i) => (
  <div key={f.id}>                         {/* f.id, not the index */}
    <input {...register(`items.${i}.title`)} />
    <button type="button" onClick={() => remove(i)}>Remove</button>
  </div>
))}

// type="button" matters — inside a form, a bare <button> submits.

Key Points to Remember

  • 1register wires an input straight to the DOM, so typing does not re-render the form
  • 2handleSubmit runs validation first and only calls your handler with valid values
  • 3formState gives errors, isSubmitting, isDirty and isValid — the flags you would otherwise track by hand
  • 4setError puts server failures into the same display as client validation errors
  • 5Controller adapts components that expose their own value/onChange instead of a DOM ref

Interview Questions

Sign in to ask Aria
1

How does React Hook Form avoid re-rendering on every keystroke?

Medium
2

How do you display a server-side validation error next to the right field?

Medium
3

When do you need Controller instead of register?

Medium

Ask Aria about React Hook Form

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…