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 1 of 4

Forms in React

Controlled vs uncontrolled, FormData, useActionState, useFormStatus, accessible errors.

Updated

What a form means in React

A form has three things to manage: the fields' values, the validation errors and the submission state (pending, success, a server error). React gives you two fundamental ways to hold the values, and every form library is built on one of them.

The HTML basics (name, label, input types, native validation) are in the HTML forms lesson — here we build on top of them.

Controlled vs uncontrolled

The difference between a controlled input and an uncontrolled one:

Controlled Uncontrolled
Where the value lives in React state (useState) in the DOM (the input holds it)
How you read it from the variable, any time on submit: FormData or a ref
Re-render on every keystroke yes no
Code value + onChange on every field just name (+ defaultValue)
When the value affects the UI in real time: live search, dependent fields, formatting (phone, card) most forms: login, contact, settings
// controlled
const [email, setEmail] = useState('')
<input value={email} onChange={e => setEmail(e.target.value)} />

// uncontrolled
<input name="email" defaultValue={user.email} />

value without onChange = a locked (read-only) input. defaultValue = the initial value, then the DOM takes over.

Reading the data: FormData

function onSubmit(e: React.FormEvent<HTMLFormElement>) {
  e.preventDefault()
  const form = new FormData(e.currentTarget)
  const email = String(form.get('email'))
  const tags = form.getAll('tags')            // checkboxes with the same name → an array
  const data = Object.fromEntries(form)       // ⚠️ keeps only the last value for repeated names
}

FormData values are always strings (or a File). A checked checkbox sends 'on', an unchecked one doesn't appear at all.

React 19: forms with actions

React 19 lets you pass a function directly to action. React takes care of preventDefault, of FormData and of the pending state.

'use client'
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'

type State = { error?: string; ok?: boolean }

async function subscribe(prev: State, formData: FormData): Promise<State> {
  const email = String(formData.get('email') ?? '')
  if (!email.includes('@')) return { error: 'Invalid email' }
  await api('/subscribe', { method: 'POST', body: formData })
  return { ok: true }
}

function SubmitButton() {
  const { pending } = useFormStatus()          // reads the parent form's state
  return <button disabled={pending}>{pending ? 'Sending…' : 'Subscribe'}</button>
}

export function Newsletter() {
  const [state, action] = useActionState(subscribe, {})
  return (
    <form action={action}>
      <input name="email" type="email" required />
      {state.error && <p role="alert">{state.error}</p>}
      <SubmitButton />
    </form>
  )
}
Hook What it gives you Where
useActionState(fn, initial) [state, action, isPending] — the result of the last submission the component with the <form>
useFormStatus() { pending, data } of the parent form a child component of the form
useOptimistic the UI updated before the response lists, likes

With a Server Action instead of the client function, the same form works even without JavaScript.

Accessible errors

An error has to be seen and heard:

<label htmlFor="email">Email</label>
<input id="email" name="email" aria-invalid={!!error} aria-describedby={error ? 'email-error' : undefined} />
{error && <p id="email-error" role="alert">{error}</p>}
Attribute Role
aria-invalid the screen reader announces "invalid"; you style it with aria-invalid:border-danger
aria-describedby links the message to the field — it's read when the user reaches the field
role="alert" / aria-live the message is announced as soon as it appears

When you need a library

The form has... Without a library With a library (React Hook Form / Conform)
2–5 fields, simple validation ✓ useActionState + FormData unnecessary
validation on blur / while typing, per-field messages clunky ✓
dynamic lists (add / remove rows) clunky ✓
a multi-step wizard, dependent fields clunky ✓

Summary

  • Uncontrolled + FormData for most forms; controlled when the value changes the UI live.
  • React 19: action={fn}, useActionState for the result, useFormStatus for pending.
  • Errors: aria-invalid + aria-describedby + role="alert".

Official sources

Exercises

Was this page helpful?

One tap — no account needed.