Command Palette

Search for a command to run...

Hectal
PHASE 5Intermediate ~22 min· topic 5 of 7

Topic 5.5

Many Fields with One State Object

In one line

Keep a form's values in one state object, give every field a name that matches a key, and update any field with one change handler using a computed property key: { ...form, [e.target.name]: e.target.value }.

Think of it like this

A school register. The teacher doesn't keep a separate notebook per pupil; there is one register with a row per name. To mark Asha present, the teacher finds the row labelled Asha and writes there. One state object for a form is that register: one object, one key per field, and the field's name tells you which row to write in.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

State object
One state value that holds several related fields: { name: "", email: "" }.
name attribute
The field's name. Here it matches the key in the state object, so one handler knows what to update.
Computed property key
A key written as [expression] in an object literal. JavaScript works the key out at run time.
Shared change handler
One onChange function used by many fields, which reads e.target.name to know which field changed.
Nested form data
Form values grouped inside objects, such as address.city.
Dynamic field list
A part of the form where the user can add and remove rows of fields.
Stable id
An identifier made once when an item is created, that never changes, used as its key.

Step by step

01One object for the whole form

List every field once, with its starting value. This object is also the shape you'll send to the server.

Profile.jsxwhole filejsx
const [form, setForm] = useState({
  name: "",
  email: "",
  role: "student",
  newsletter: false,
});

02Name every field after its key

name and the key must match exactly, including capital letters.

Profile.jsxwhole filejsx
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<select name="role" value={form.role} onChange={handleChange}>…</select>
<input type="checkbox" name="newsletter" checked={form.newsletter} onChange={handleChange} />

03Write one handler

Pick checked or value by type, then copy the object with one key replaced.

Profile.jsxwhole filejsx
function handleChange(e) {
  const { name, value, type, checked } = e.target;
  setForm((f) => ({ ...f, [name]: type === "checkbox" ? checked : value }));
}
Write one handlerdiagram
Rendering diagram…

04Nested values need a copy per level

A dotted name tells the handler which group the field belongs to.

Profile.jsxwhole filejsx
function handleAddress(e) {
  const key = e.target.name.split(".")[1];       // "address.city" -> "city"
  setForm((f) => ({ ...f, address: { ...f.address, [key]: e.target.value } }));
}

05Rows you can add and remove

Each row is an object with its own id. Change and remove by id; key the inputs by id.

Guests.jsxwhole filejsx
let nextId = 1;
const [guests, setGuests] = useState([]);

function add() {
  const id = nextId++;
  setGuests((gs) => [...gs, { id, name: "" }]);
}
const rename = (id, name) => setGuests((gs) => gs.map((g) => (g.id === id ? { ...g, name } : g)));
const remove = (id) => setGuests((gs) => gs.filter((g) => g.id !== id));

Try it yourself

  1. 1

    Add a field in two lines

    In the profile form, add phone: "" to initial and <input name="phone" value={form.phone} onChange={handleChange} />. No new handler is needed.

  2. 2

    Misspell a name

    Change the email input's name to Email (capital E) and type in it. Predict what appears in the JSON. (A new key Email appears and email stays empty, while the field itself stays empty because it shows form.email.)

  3. 3

    Key the guests by index

    Change key={g.id} to key={i}. Type different names in three rows, put the cursor in row 3, and remove row 1. The text still follows the data, but focus and any uncontrolled state stay on the wrong positions. Put g.id back.

Code & diagrams

One state object, one change handler New tab

Six fields, two handlers. The JSON under the form is the state, live. Type in any field and watch exactly one key change.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

{ name: 'Asha', email: '', role: 'student', newsletter: true, address: { city: 'Pune', pin: '' } }
A dynamic list of guests New tab

Two rows were added from code. Type a name into each row, then remove the first one: the other rows keep their own text, because they are keyed by id, not by position.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

rows: 3

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget the brackets around the key

In the profile form, change [name]: ... to name: ... in handleChange, then type Asha in the Email field.

terminal
$ Preview
── what you'll see ──
The Name field shows Asha and the Email field stays empty. The JSON shows name: "Asha", email: "".

Break #2

Forget to spread the old values

In a form with flat fields { name: "", email: "" }, write the handler as setForm({ [name]: value }) without the spread, then type one letter in Name.

terminal
$ Console
── what you'll see ──
A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://react.dev/link/controlled-components

Myth vs fact

Myth

One state object is always better than many useState calls.

Fact

One object suits fields that are edited and sent together. Unrelated values (a modal's open flag, a search box) are clearer in their own state.

Myth

Setting state merges the new keys into the old object, like class components did.

Fact

useState setters replace the value. You must spread the old object yourself.

Myth

The array index is fine as a key for form rows.

Fact

Rows that can be removed or reordered need stable ids; otherwise focus, uncontrolled values and component state stick to the wrong row (Topic 3.3).

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    With one object, every keystroke re-renders the whole form component and all its fields. For forms with dozens of fields that can be noticeable; split sections into child components, wrap them in memo (Phase 16), or use an uncontrolled approach such as React Hook Form (Topic 5.7). The React Compiler can remove much of this cost automatically.

  • ▸

    Reading e.target inside the updater function is safe in React 17+ (no more event pooling), but destructuring name/value first is clearer and survives later refactors that make the handler async.

  • ▸

    Module-level let nextId is fine for a demo but shared between all instances and reset on reload. Real apps use crypto.randomUUID() when the row is created, or ids from the server.

  • ▸

    useReducer (Topic 9.4) is a good fit for large forms: actions like field_changed, row_added and reset keep the update rules in one tested function.

Remember this

  1. 1

    With ten fields, ten useState calls and ten handlers are repetitive. Instead keep one object: useState({ name: "", email: "", city: "" }). Each field shows its own key, value={form.email}, and carries a name attribute with the same text: name="email".

  2. 2

    Now one handler can serve every field. e.target.name says which field changed and e.target.value what it now holds: setForm({ ...form, [e.target.name]: e.target.value }). The spread ...form copies all the old keys (state must be replaced, not changed, Topic 4.6), and the new key overwrites one of them.

  3. 3

    [e.target.name] in an object literal is a computed property key: JavaScript works out the key from the expression in the brackets. { ["em" + "ail"]: 1 } is { email: 1 }. Without the brackets, { name: value } would always set a key literally called name.

  4. 4

    Checkboxes report checked, not value, so a shared handler checks the type: const v = type === "checkbox" ? checked : value;. Using the updater form, setForm((f) => ({ ...f, [name]: v })), keeps updates correct even when several happen in one event (Topic 4.5). Read name, value and checked from e.target before the updater runs.

  5. 5

    Nested form data such as address: { city, pin } needs a copy at each level: { ...form, address: { ...form.address, [name]: value } }. A common trick is a dotted name, name="address.city", which the handler splits. Deep nesting gets awkward fast; keep form state as flat as you can (Topic 6.1), or use Immer (Topic 4.6).

  6. 6

    Dynamic field lists (several phone numbers, guests, line items) are an array in state. Give each row a stable id made when the row is added (not the array index, Topic 3.3), and update rows by id: add with [...rows, { id, name: "" }], change with rows.map((r) => r.id === id ? { ...r, name } : r), remove with rows.filter((r) => r.id !== id). With index keys, removing a row makes the inputs below it show the wrong text or keep the wrong focus.

Explain it without notes

01

How does one change handler update the right field in a state object?

02

What is a computed property key, and why does the form handler need it?

03

How do you update a nested field like address.city without mutating state?

04

How should you manage a list of rows that users can add and remove?

Practice

01

Build a booking form with name, date and seats in one state object and one handler, showing Asha booked 2 seats on 2024-01-15 below it.

02

Build a list of email inputs with Add and Remove buttons, stored as [{ id, email }].

Trade-offs

  • ↔

    One object and one handler cut repetition and match the shape you send to the server, but every change re-renders the whole form and a typo in a name fails silently.

  • ↔

    Nesting mirrors the server's data shape but makes updates verbose; flat state with names like addressCity is simpler to update and to validate.

Done when you can

  • I can manage many fields with one state object and one handler.

  • I can explain computed property keys.

  • I handle checkboxes in a shared handler.

  • I can update nested form data immutably.

  • I can build add/edit/remove rows keyed by stable ids.