All topics
Patternsintermediate

Controlled Components at Scale (Form Composition)

Learn how to compose multiple controlled fields into a larger form structure without prop-drilling every handler individually.

As a form grows beyond a couple of fields, wiring up individual useState and onChange handlers for each input becomes repetitive and error-prone. A common pattern is to consolidate all field values into a single state object (or use useReducer) and write one generic change handler that updates the correct field by name, often via the input's name attribute.

It's like using one universal remote control with labeled buttons (each input's `name`) instead of building a separate custom remote for every single appliance in the house — one piece of logic reads the label and knows exactly which device (field) to adjust.

Key Concepts

1
This generalized handler pattern (handleChange(e) { setValues(v => ({...v, [e.target.name]: e.target.value})) }) scales much better than one handler per field, since adding a new field only requires adding a name attribute to the JSX, not writing a new handler function. It also keeps all form state colocated in one object, which is convenient for validation and submission logic that needs to see the whole form's values together.
handleChange(e) { setValues(v => ({...v, [e.target.name]: e.target.value})) }name
2
For genuinely large or deeply nested forms, many teams reach for a dedicated form library (see the Form Libraries topic) specifically to avoid re-implementing validation, per-field error state, and touched/dirty tracking by hand on top of this pattern, since those concerns multiply in complexity as a form grows.
3
Interviewers sometimes ask candidates to refactor a form with five separate useState calls and five separate handlers into a single object-based state with one generic handler, checking for correct use of the computed property name syntax and awareness of when the DIY approach should be abandoned for a library instead.
useState