webroad.online
  1. 1Web
  2. 2HTML
  3. 3CSS
  4. 4JavaScript
  5. 5TypeScript
  6. 6Git
  7. 7Tooling
  8. 8React
  9. 9State management
  10. 10Next.js
  11. 11Forms
  12. 12Data and backend
  13. 13SEO
  14. 14Tailwind CSS
  15. 15Animations
  16. 16Testing
  17. 17Architecture
Forms · Lesson 2 of 4

React Hook Form

useForm, register, handleSubmit, formState, Controller, useFieldArray, the validation modes.

Updated

What React Hook Form is

React Hook Form (RHF) is the most used form library for React. A single hook, useForm, gives you everything you need: registering fields, validation, errors, the submission state.

The secret of its performance: it's built on uncontrolled fields. The values live in the DOM, not in React state → typing does not re-render the whole form. With 50 fields, you feel the difference.

'use client'
import { useForm } from 'react-hook-form'

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

export function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<Values>({ defaultValues: { email: '', password: '' } })

  const onSubmit = async (values: Values) => {
    await login(values)
  }

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

      <input type="password" {...register('password', { minLength: { value: 8, message: 'At least 8 characters' } })} />
      {errors.password && <p role="alert">{errors.password.message}</p>}

      <button disabled={isSubmitting}>Log in</button>
    </form>
  )
}

The pieces of useForm

Piece What it does
register('name', rules) connects an input: returns name, ref, onChange, onBlur
handleSubmit(fn) validates, then calls fn(values) only if everything is valid
formState.errors the errors per field: errors.email?.message
formState.isSubmitting true while onSubmit (async) runs
formState.isDirty / dirtyFields what changed compared to defaultValues
formState.isValid the form is valid right now
watch('name') subscribes to a value — re-renders when it changes (use sparingly)
setValue, reset, setError control from code: filling in, resetting, an error coming from the server

When it validates — the mode

useForm({ mode: 'onTouched' })
mode Validates The experience
onSubmit (default) on submit, then on every change errors only after the first submit
onBlur when you leave the field —
onTouched after the first blur, then on every change the friendliest — doesn't yell while you're still typing
onChange on every keystroke aggressive, expensive

Components that aren't native inputs: Controller

register works with <input>, <select>, <textarea>. For custom components (a date picker, a select from a UI library) you use Controller, which turns them into controlled fields:

<Controller
  control={control}
  name="country"
  render={({ field, fieldState }) => <CountrySelect {...field} error={fieldState.error?.message} />}
/>

Dynamic lists: useFieldArray

const { fields, append, remove } = useFieldArray({ control, name: 'phones' })

{fields.map((field, i) => (
  <div key={field.id}>                                   {/* field.id, not the index */}
    <input {...register(`phones.${i}.number`)} />
    <button type="button" onClick={() => remove(i)}>Delete</button>
  </div>
))}
<button type="button" onClick={() => append({ number: '' })}>Add a phone</button>

Validation with a schema

Rules written in register get scattered across the JSX. The recommended way: a Zod schema + zodResolver — the types are inferred from the schema, and the same schema validates on the server too. Details in the lesson on Zod.

const form = useForm({ resolver: zodResolver(loginSchema), mode: 'onTouched' })

Common mistakes

  • watch() without an argument in the main component → re-renders the whole form on every keystroke. Use useWatch({ name }) in a small component.
  • key={index} in useFieldArray — use field.id.
  • Validation only in RHF, without validation on the server.

Summary

  • useForm → register on the fields, handleSubmit(onSubmit), formState.errors.
  • Uncontrolled under the hood → fast; Controller for custom components, useFieldArray for lists.
  • mode: 'onTouched' + a Zod schema with zodResolver.

Official sources

Exercises

Was this page helpful?

One tap — no account needed.