ReactHandling Multiple Inputs

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:

JSX
// 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:

JSX
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>
  )
}
Note
The spread `{ ...prev, [name]: value }` is essential. React state objects must be replaced (not mutated), and the spread ensures all other fields remain unchanged when one field updates.
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:

JSX
// ✓ 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:

JSX
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:

JSX
<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:

JSX
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>
  )
}
Tip
The `<pre>{JSON.stringify(form, null, 2)}</pre>` block is a handy debugging trick while building forms. Remove it before shipping.
Resetting the Form

Resetting all fields is trivial with the single-object approach — just call setForm(initialForm):

JSX
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:

JSX
// 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 name attribute matching the corresponding key in the state object — this is what links the generic handler to the right field.

  • Each input must have a value prop (for text/email/tel/number) or checked prop (for checkboxes) tied to the state — this is what makes it a controlled component.

  • Use type === "checkbox" to branch between e.target.checked and e.target.value.

  • Multi-select inputs need special handling: read Array.from(e.target.selectedOptions).map(o => o.value).

Warning
Radio button groups also require special handling. Each radio shares the same `name`, and only one can be selected. Use `e.target.value` just like a text input — the browser ensures only the selected radio fires the change event.