skip to content

A React sign-up form has grown to a dozen inputs, each with its own useState call. What do you gain and what do you give up by collapsing them into a single state object updated by one generic change handler?

level: middleimportance: should knowfreq 62%

answer

  1. one setter or twelve
  2. handler keyed by the input's name
  3. the setter replaces, it does not merge
  4. spread is shallow, nesting is manual
  5. render count barely differs

basics

~20 s

One state object plus a name-keyed change handler removes a dozen useState calls and makes reset, prefill and submit trivial. The price is spreading the previous object on every update and losing per-field bail-outs; re-render counts barely change.

solid answer

~50 s

I start with separate `useState` calls because each one is independently readable and there is nothing to get wrong. Once the boilerplate hurts, or I need to treat the values as a unit — reset them, prefill them from a fetch, hand the whole shape to the submit call — I move to one object and one handler: `setValues(prev => ({ ...prev, [e.target.name]: e.target.value }))`, with every input carrying a `name`. The costs are real. A `useState` setter replaces rather than merges, so forgetting the spread silently wipes the other fields, and nested shapes need nested copies. What it does *not* cost is extra rendering: if all twelve `useState` calls live in the same component, that component re-renders on any change either way. The only render difference is that a fresh object is never `Object.is`-equal to the old one, so React can no longer bail out when a value is set to what it already was.

code

javascript · 22 lines
javascript
import { useState } from 'react';

export default function SignupForm() {
  const [values, setValues] = useState({ email: '', password: '', newsletter: false });

  function handleChange(e) {
    const { name, value, type, checked } = e.target;
    setValues(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value }));
  }

  return (
    <form onSubmit={e => { e.preventDefault(); console.log(values); }}>
      <input name="email" value={values.email} onChange={handleChange} />
      <input name="password" type="password" value={values.password} onChange={handleChange} />
      <input name="newsletter" type="checkbox" checked={values.newsletter} onChange={handleChange} />
      <button type="submit">Sign up</button>
      <button type="button" onClick={() => setValues({ email: '', password: '', newsletter: false })}>
        Reset
      </button>
    </form>
  );
}

go deeper

for a junior

Be ready to write the name-keyed change handler from memory, including the previous-value spread, and to say that each input needs a matching name attribute.

for a middle

Explain that a useState setter replaces state rather than shallow-merging it like class setState, that the spread is shallow so nested shapes need nested copies, and when the boilerplate justifies the switch.

for a senior

Show judgment about where the state lives: pushing state into field components is what reduces render work, while packing it into one object is about reset, prefill and cross-field rules. Name the point where a reducer or a library takes over.

for a principal

Own the consistency question — mixed form styles across a codebase cost more in review time than either shape costs at runtime. Be ready to argue for one house pattern and the migration path to it.

## The two shapes A form's values can live as one `useState` per field, or as one `useState` holding an object: ```jsx // per-field const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); // one object const [values, setValues] = useState({ email: '', password: '' }); ``` Both are idiomatic React. The choice is about how much of the form you need to move around as a single thing, not about which one React prefers. ## What the object shape buys The first win is the generic handler. Give every input a `name` that matches a key, and one function serves the whole form: ```jsx function handleChange(e) { const { name, value, type, checked } = e.target; setValues(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value })); } ``` Adding a field becomes adding one input and one key, not one `useState`, one handler and one prop. The second win is unit operations. Resetting is `setValues(initialValues)`. Prefilling from a fetched profile is one `setValues(profile)` instead of a dozen setters that must not drift out of step. Submitting is `JSON.stringify(values)` rather than assembling an object by hand at the call site. Anything that treats the form as one value — a draft saved to storage, a dirty check against the initial object, a validator that needs cross-field rules like "password must match confirmation" — gets simpler, because there is one value to pass. ## What it costs The classic bug is assuming the setter merges. In a class component `this.setState({ email })` shallow-merges into existing state. The `useState` setter does not: whatever you pass **becomes** the state. So `setValues({ email: next })` does not update the email field — it replaces the entire object, and every other key is now `undefined`, which typically shows up as React's controlled-to-uncontrolled warning as inputs receive `undefined`. The spread is mandatory, and it must be inside a functional update (`prev => ...`) if several updates can be queued in the same batch. The spread is also shallow. If the shape nests — `{ address: { city, zip } }` — then `{ ...prev, address: { ...prev.address, city } }` is required at every level, and that nesting is exactly where hand-rolled forms start to feel painful. ## The re-render claim, corrected Candidates often say the object shape "re-renders the whole form on every keystroke" as if the per-field shape did not. If all the state lives in the same component, both shapes re-render that component on any change, and React re-runs its whole JSX either way. The shapes are equivalent here. What genuinely reduces rendering work is *where* the state lives, not how it is packed: state owned by a child field component only re-renders that child. That is a state-placement decision, and it is orthogonal to the object-versus-per-field question. There is one real render difference. React can skip re-rendering when the next state is `Object.is`-equal to the current one, so `setEmail('ada')` when email is already `'ada'` bails out. A spread always produces a fresh object reference, which is never `Object.is`-equal, so the equivalent object update always renders. In a change handler the value has usually changed anyway, so this matters mostly for programmatic writes — restoring a draft, echoing a server response — where you may want to compare before setting. ## Choosing Under about four or five independent fields, per-field state usually reads better: each piece of state is named, its type is obvious, and there is no indirection between an input and its value. Reach for one object when the field count makes the boilerplate the dominant cost, when the form needs reset/prefill/dirty-check as a unit, or when validation is cross-field and wants the whole shape in one place. A third position exists between the two: keep the object, but stop driving it with `useState` and derive the next shape from a reducer, so field updates become named actions. That is the point at which many teams also start asking whether a form library should own the shape entirely — the same forces (many fields, shared validation rules, dirty and touched tracking) push in that direction. ## The tell in an interview Saying "one object, one handler, and remember the setter replaces instead of merging" already puts you ahead of most answers. Adding "and it does not actually change how much re-renders" shows you know why the popular version of the tradeoff is wrong.

  • What exactly goes wrong if the handler writes setValues({ [name]: value }) without the spread?
    That call replaces the whole state object, so every key except the one you wrote becomes `undefined`. On the next render each of those inputs receives `value={undefined}` and flips to uncontrolled, which React warns about, and the user watches the rest of the form blank itself out. The spread — inside a `prev =>` updater so queued updates compose — is what preserves the other fields.
  • Does packing form values into one object make the form re-render more than separate useState calls?
    Not on its own. If both shapes live in the same component, any update re-renders that component and re-runs all its JSX. The only difference is bail-out: React can skip a render when the next state is `Object.is`-equal to the current one, and a freshly spread object never is. Real render savings come from moving state into child field components, which is a placement decision, not a packing one.
  • When would you stop at one object and reach for a reducer instead?
    When updates stop being "set this key to this value". Once a change to one field must clear another's error, flip a step, or enforce a rule across fields, a reducer gives those transitions names and keeps the logic in one place instead of scattered across handlers. Nested shapes also get easier to keep consistent when a single function owns every transition.

saying these in an interview costs you the question

  • Thinks the useState setter merges like class this.setState
  • Claims one object always beats per-field state
  • Says one object re-renders the form more than separate useState calls
  • Forgets the previous-value spread and loses the other fields
  • Assumes the spread deep-copies nested field objects

context