Handling Multiple Inputs
When a form has more than one or two fields, creating a separate useState call and a separate onChange handler for each field quickly becomes repetitive. React gives you a cleaner pattern: store all field values in one state object and write one generic handler that uses e.target.name to update the right field.
The Naïve Approach (Does Not Scale)
It is tempting to start with individual state variables for each field:
// Works, but bloats fast with many fields
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [email, setEmail] = useState('')
const [phone, setPhone] = useState('')
// Then four separate handlers...
<input value={firstName} onChange={e => setFirstName(e.target.value)} name="firstName" />
<input value={lastName} onChange={e => setLastName(e.target.value)} name="lastName" />
// and so on...With ten fields that is ten state variables and ten handlers. There is a much better way.
One State Object, One Handler
Group all field values into a single object and use a computed property key ([e.target.name]) to update only the changed field:
import { useState } from 'react'
const initialState = {
firstName : '',
lastName : '',
email : '',
phone : '',
}
function RegistrationForm() {
const [form, setForm] = useState(initialState)
// One handler handles ALL text/email/tel/number inputs
function handleChange(e) {
const { name, value } = e.target
setForm(prev => ({ ...prev, [name]: value }))
}
function handleSubmit(e) {
e.preventDefault()
console.log('Submitted:', form)
}
return (
<form onSubmit={handleSubmit}>
<input
name="firstName"
value={form.firstName}
onChange={handleChange}
placeholder="First name"
/>
<input
name="lastName"
value={form.lastName}
onChange={handleChange}
placeholder="Last name"
/>
<input
name="email"
type="email"
value={form.email}
onChange={handleChange}
placeholder="Email"
/>
<input
name="phone"
type="tel"
value={form.phone}
onChange={handleChange}
placeholder="Phone"
/>
<button type="submit">Register</button>
</form>
)
}Why the Functional Update Form?
Notice the handler uses setForm(prev => ...) rather than
setForm({ ...form, [name]: value }). Using the previous-state
callback is safer when multiple updates might be batched:
// ✓ Safe — always based on the latest state
setForm(prev => ({ ...prev, [name]: value }))
// ✗ Can stale — captures 'form' from the closure at the time the handler ran
setForm({ ...form, [name]: value })Handling Checkboxes
Checkboxes store a boolean, not a string. Their event object uses e.target.checked instead of e.target.value. Extend the handler with a type check:
function handleChange(e) {
const { name, value, type, checked } = e.target
setForm(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}))
}
// In the JSX:
<label>
<input
type="checkbox"
name="agreeToTerms"
checked={form.agreeToTerms}
onChange={handleChange}
/>
I agree to the terms and conditions
</label>Handling a Select Element
A <select> behaves just like a text input — e.target.value is the selected option's value, and the same generic handler works without any changes:
<select
name="country"
value={form.country}
onChange={handleChange} // same handler!
>
<option value="">-- Select country --</option>
<option value="ca">Canada</option>
<option value="us">United States</option>
<option value="gb">United Kingdom</option>
</select>Complete Registration Form Example
Here is a full working form with text inputs, a select, and a checkbox, all managed by a single state object and a single handler:
import { useState } from 'react'
const initialForm = {
firstName : '',
lastName : '',
email : '',
role : 'viewer',
newsletter : false,
}
function RegistrationForm() {
const [form, setForm] = useState(initialForm)
const [submitted, setSubmitted] = useState(false)
function handleChange(e) {
const { name, value, type, checked } = e.target
setForm(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}))
}
function handleSubmit(e) {
e.preventDefault()
setSubmitted(true)
console.log('Form data:', form)
}
if (submitted) {
return (
<div>
<h2>Welcome, {form.firstName}!</h2>
<p>We will contact you at {form.email}.</p>
</div>
)
}
return (
<form onSubmit={handleSubmit} style={{ display: 'grid', gap: 12 }}>
<div>
<label>First name *</label>
<input
name="firstName"
value={form.firstName}
onChange={handleChange}
required
/>
</div>
<div>
<label>Last name *</label>
<input
name="lastName"
value={form.lastName}
onChange={handleChange}
required
/>
</div>
<div>
<label>Email *</label>
<input
name="email"
type="email"
value={form.email}
onChange={handleChange}
required
/>
</div>
<div>
<label>Role</label>
<select name="role" value={form.role} onChange={handleChange}>
<option value="viewer">Viewer</option>
<option value="editor">Editor</option>
<option value="admin">Admin</option>
</select>
</div>
<label>
<input
type="checkbox"
name="newsletter"
checked={form.newsletter}
onChange={handleChange}
/>
{' Subscribe to newsletter'}
</label>
<button type="submit">Create Account</button>
{/* Debug: preview state while developing */}
<pre style={{ fontSize: 12, background: '#f5f5f5', padding: 8 }}>
{JSON.stringify(form, null, 2)}
</pre>
</form>
)
}Resetting the Form
Resetting all fields is trivial with the single-object approach — just call setForm(initialForm):
function handleReset() {
setForm(initialForm)
}
// Or wire it to the form's native reset event:
<form onReset={handleReset}>
{/* ... */}
<button type="reset">Clear</button>
</form>Extracting a Reusable Hook
If the same pattern appears across many forms, extract it into a custom hook:
// hooks/useForm.js
import { useState } from 'react'
export function useForm(initialValues) {
const [form, setForm] = useState(initialValues)
function handleChange(e) {
const { name, value, type, checked } = e.target
setForm(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value,
}))
}
function reset() {
setForm(initialValues)
}
return { form, handleChange, reset }
}
// Usage in any component:
function ContactForm() {
const { form, handleChange, reset } = useForm({
name : '',
message : '',
urgent : false,
})
// form, handleChange, and reset are ready to use
}Each input must have a
nameattribute matching the corresponding key in the state object — this is what links the generic handler to the right field.Each input must have a
valueprop (for text/email/tel/number) orcheckedprop (for checkboxes) tied to the state — this is what makes it a controlled component.Use
type === "checkbox"to branch betweene.target.checkedande.target.value.Multi-select inputs need special handling: read
Array.from(e.target.selectedOptions).map(o => o.value).