Learn ReactJS - Forms & Validation
Episode 11 of 24

Learn ReactJS - Forms & Validation

This episode covers form handling with controlled and uncontrolled components, then React Hook Form with validation rules and schema validation using Yup or Zod. You'll also learn form accessibility and clear input feedback.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Forms are the main gateway for user data, but also the most common source of bugs: inconsistent validation, confusing error messages, and neglected accessibility. Episode 11 makes you master React forms thoroughly, starting with the basics — controlled and uncontrolled components — then moving to React Hook Form with schema validation using Yup or Zod, and finishing with accessibility rules and input feedback that's comfortable for everyone.

Form Handling with Controlled and Uncontrolled Components

Controlled: State Controls the Value

As in episode 5, a controlled component puts the input value in state:

JSSimple controlled form
import { useState } from "react"
 
function Form() {
  const [nama, setNama] = useState("")
 
  const submit = (e) => {
    e.preventDefault()
    console.log("Dikirim:", nama)
  }
 
  return (
    <form onSubmit={submit}>
      <input value={nama} onChange={(e) => setNama(e.target.value)} />
      <button type="submit">Kirim</button>
    </form>
  )
}

e.preventDefault() stops the default page reload, then handleSubmit processes the data. Controlled components fit best when the input value is used for immediate validation or other UI.

Uncontrolled: Refs for Values

For simple, one-shot forms, uncontrolled components use ref and read the value at submit time, without re-rendering on every keystroke:

JSUncontrolled form with ref
import { useRef } from "react"
 
function Form() {
  const namaRef = useRef(null)
 
  const handleSubmit = (e) => {
    e.preventDefault()
    console.log("Dikirim:", namaRef.current.value)
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input ref={namaRef} defaultValue="" />
      <button type="submit">Kirim</button>
    </form>
  )
}

namaRef.current.value reads the input value at submit time. Without onChange, the component doesn't re-render on every keystroke — more efficient for large forms. React Hook Form in the next section uses this approach internally.

React Hook Form and Validation Rules

The useForm Hook

React Hook Form combines uncontrolled speed with easy validation:

Install React Hook Form
npm install react-hook-form
JSForm with useForm
import { useForm } from "react-hook-form"
 
function Form() {
  const { register, handleSubmit, formState } = useForm()
  const { errors } = formState
 
  const onSubmit = (data) => console.log(data)
 
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email", { required: "Email wajib diisi" })} />
      {errors.email && <p>{errors.email.message}</p>}
      <button type="submit">Kirim</button>
    </form>
  )
}

register("email", { required: "Email wajib diisi" }) registers the input to the form with validation rules. handleSubmit(onSubmit) validates first — the onSubmit function only runs when all rules pass, and errors are available in formState.errors.

Built-in Validation Rules

Even without a schema, React Hook Form provides basic rules like min, max, and maxLength written inside register, just like required in the example above. For more complex, reusable rules, the schema validation in the next section is the cleaner choice.

Form Schema Validation with Yup or Zod

Zod: Schema and TypeScript

Schema validation defines the rules once, then uses them for both validation and types. Zod is the modern choice:

Install Zod and resolver
npm install zod @hookform/resolvers
JSForm validation with Zod
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
 
const skema = z.object({
  email: z.string().email("Format email salah"),
  password: z.string().min(8, "Minimal 8 karakter"),
})
 
function Form() {
  const { register, handleSubmit, formState } = useForm({
    resolver: zodResolver(skema),
  })
 
  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <input {...register("email")} />
      {formState.errors.email && <p>{formState.errors.email.message}</p>}
      <input type="password" {...register("password")} />
      {formState.errors.password && <p>{formState.errors.password.message}</p>}
      <button type="submit">Daftar</button>
    </form>
  )
}

zodResolver(skema) connects the Zod schema to React Hook Form. z.object, z.string().email, and z.string().min define the rules; error messages are pulled automatically from the schema. In TypeScript, the form data type can be inferred directly from the schema. Yup has a similar API with string().email().min(8) — choosing between them is a matter of preference, and both run through the same @hookform/resolvers.

Form Accessibility and Input Feedback

Labels, aria, and Error Messages

An accessible form needs three things: a label connected to the input, aria-* attributes that describe status, and error messages a screen reader can read:

JSAccessible form
<form onSubmit={handleSubmit(onSubmit)} noValidate>
  <label htmlFor="email">Email</label>
  <input
    id="email"
    {...register("email", { required: "Email wajib diisi" })}
    aria-invalid={errors.email ? "true" : "false"}
  />
  {errors.email && <p role="alert">{errors.email.message}</p>}
</form>

htmlFor and id connect the label to the input. aria-invalid tells the screen reader that the input has a problem, and role="alert" announces the error message when it appears. noValidate disables the browser's built-in validation so React Hook Form handles it consistently.

Conclusion

Episode 11 made you proficient in React forms: the difference between controlled and uncontrolled components, React Hook Form with validation rules, schema validation with Zod and Yup, plus accessibility and user-friendly input feedback.

Key takeaways:

  • Controlled stores the value in state; uncontrolled uses refs and renders less.
  • React Hook Form combines performance with clean validation.
  • Validation rules are declared in register; errors appear in formState.errors.
  • Schema validation with Zod or Yup unifies rules and data types.
  • htmlFor, aria-invalid, and role="alert" make forms accessible.
  • Clear error feedback reduces user confusion.

In the next episode, episode 12, we'll cover secure frontend & auth patterns — authentication patterns for SPAs, JWT and refresh tokens with secure storage, protecting routes and role-based access control, and mitigating CSRF and XSS when handling user input. Your app starts facing real security.

Learn ReactJS - Forms & Validation | Learn ReactJS