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.currentTargetfor the element the handler is attached to andevent.targetfor 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
onSelectoronClosekeeps 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>
)
}| Input | Controlled form | Notes |
|---|---|---|
| text, email, password | value + onChange | The value is always a string |
| checkbox | checked + onChange | Read event.target.checked, not value |
| radio group | checked per option | All options share one name and one state field |
select | value on the select | Put value on the select, not on the option |
number | value as a string | An empty field is ''; parse on submit |
file | Uncontrolled only | The 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-describedbyso screen readers announce it. - Set
aria-invalidon a field with an error and give the messagerole="alert"- do not rely on colour alone. - A form action gets a pending state for free in the surrounding Suspense boundary; a plain
onSubmithandler 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.Related
Component composition and reusable APIs Server components, actions and the use API
Last refreshed 2026-09-18.