Events, forms and controlled inputs

Synthetic events and handler references, controlled versus uncontrolled inputs, file inputs that cannot be controlled, and server-side validation.

Events and handlers

function SearchBox({ onSearch }) {
  // a handler receives a SyntheticEvent: React's cross-browser wrapper
  function handleSubmit(event) {
    event.preventDefault()                       // stop the browser navigation
    const data = new FormData(event.currentTarget)
    onSearch(String(data.get('q') ?? '').trim())
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="q" type="search" placeholder="Search guides" />
      <button type="submit">Search</button>
    </form>
  )
}

// pass the function itself, not the result of calling it
<button onClick={handleSubmit}>Submit</button>
// <button onClick={handleSubmit()}>  // runs on every render
  • React attaches one listener at the root and dispatches through its own tree, so event.stopPropagation() stops React handlers, not just DOM ones.
  • preventDefault() cancels the browser action (form submit, link navigation) and is unrelated to propagation.
  • Read event.currentTarget for the element the handler is attached to and event.target for the element that was actually clicked - the difference matters in a form or a delegated list.
  • React 19 no longer pools events, so the event object stays valid after the handler returns and can be read inside an async function.
  • Handlers are ordinary props: a component that takes onSelect or onClose keeps its caller in control of what happens.

Controlled and uncontrolled inputs

import { useState } from 'react'

function Signup() {
  const [form, setForm] = useState({ email: '', plan: 'free', terms: false })

  function handleChange(event) {
    const { name, value, type, checked } = event.target
    setForm(previous => ({ ...previous, [name]: type === 'checkbox' ? checked : value }))
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Email
        <input name="email" type="email" value={form.email} onChange={handleChange} />
      </label>

      <label>
        Plan
        <select name="plan" value={form.plan} onChange={handleChange}>
          <option value="free">Free</option>
          <option value="pro">Pro</option>
        </select>
      </label>

      <label>
        <input name="terms" type="checkbox" checked={form.terms} onChange={handleChange} />
        Accept the terms
      </label>

      <button disabled={!form.terms}>Create account</button>
    </form>
  )
}
InputControlled formNotes
text, email, passwordvalue + onChangeThe value is always a string
checkboxchecked + onChangeRead event.target.checked, not value
radio groupchecked per optionAll options share one name and one state field
selectvalue on the selectPut value on the select, not on the option
numbervalue as a stringAn empty field is ''; parse on submit
fileUncontrolled onlyThe browser refuses to let React set the value

A controlled input has a value prop and no matching onChange renders a read-only field - the classic symptom of forgetting the handler. Use uncontrolled inputs with defaultValue, or plain name attributes plus FormData, when you only need the value at submit time: it is less code and fewer re-renders.

Form actions, validation and file inputs

// a form action receives FormData, and React resets the form after a successful submit
function NewsletterForm() {
  async function subscribe(formData) {
    const response = await fetch('/api/subscribe', { method: 'POST', body: formData })
    if (!response.ok) throw new Error('Could not subscribe')
  }

  return (
    <form action={subscribe}>
      <input name="email" type="email" required />
      <button type="submit">Subscribe</button>
    </form>
  )
}

// a file input cannot be controlled: read the File from the element
function AvatarUpload() {
  const [preview, setPreview] = useState(null)

  function handleChange(event) {
    const file = event.target.files?.[0]
    if (!file) return
    setPreview(URL.createObjectURL(file))
    event.target.value = ''            // allows re-selecting the same file
  }

  useEffect(() => () => preview && URL.revokeObjectURL(preview), [preview])

  return (
    <>
      <input type="file" accept="image/*" onChange={handleChange} />
      {preview && <img src={preview} alt="Selected avatar" />}
    </>
  )
}
  • Show a validation message next to the field that produced it, and connect it with aria-describedby so screen readers announce it.
  • Set aria-invalid on a field with an error and give the message role="alert" - do not rely on colour alone.
  • A form action gets a pending state for free in the surrounding Suspense boundary; a plain onSubmit handler needs its own loading flag.
  • Object URLs are a memory leak if they are never revoked, which is why the cleanup belongs in the same component that created them.
⚠️
Client-side validation is a usability feature, not a control. required, pattern and every JavaScript check can be bypassed with a plain HTTP request, so the same rules must run on the server before anything is stored.

FAQ

Controlled or uncontrolled inputs?
Controlled when the value is needed during render - live validation, a disabled submit button, derived previews. Uncontrolled with a ref or FormData when you only need the value on submit, which is less code and re-renders less.
Why does my input lose focus while typing?
Usually a component defined inside its parent's render body, or a changed key, which remounts the element every render. Move the component out and give the node a stable identity.

Component composition and reusable APIs Server components, actions and the use API

Last refreshed 2026-09-18.