Difficulty: Intermediate
When forms have more than a couple of fields, managing a separate useState for each field becomes verbose and repetitive. A better pattern is to use a single state object that holds all form values, with a generic onChange handler that updates the correct field using computed property names. This pattern scales well to forms of any size.
The generic handler pattern uses the input's name attribute to determine which state property to update. The handler reads event.target.name and event.target.value, then updates the state using the computed property syntax: setForm(prev => ({ ...prev, [name]: value })). This single handler works for any number of text inputs, selects, and textareas, as long as each has a unique name attribute matching its key in state.
For checkboxes, the handler needs to check whether the input type is a checkbox and use event.target.checked instead of event.target.value. A common approach is: const value = type === 'checkbox' ? checked : targetValue. This makes the generic handler work with all standard form element types.
Using a single state object also makes form reset, form population (from API data), and form comparison easier. To reset the form, set state back to the initial object. To populate from an API response, set state to the response data. To check if the form has been modified, compare the current state with the initial state using JSON.stringify or a deep comparison.
For very complex forms with many interdependent fields, validation states, and submission logic, useReducer can replace useState. The reducer centralizes all state transitions (field updates, validation, submission, reset) in one place. This is especially valuable when field changes trigger cascading updates to other fields - the reducer can handle all related changes in a single dispatch.
function ContactForm() {
const [form, setForm] = React.useState({
name: '',
email: '',
message: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
console.log('Updated ' + name + ':', value);
};
// Simulate filling out the form
handleChange({ target: { name: 'name', value: 'Alice' } });
handleChange({ target: { name: 'email', value: 'alice@test.com' } });
handleChange({ target: { name: 'message', value: 'Hello!' } });
console.log('Form fields:', Object.keys(form).length);
return null;
}
ContactForm();
A single state object holds all form values. The generic handleChange uses computed property names ([name]: value) to update the correct field. One handler works for all text-like inputs.
function SettingsForm() {
const [settings, setSettings] = React.useState({
username: '',
notifications: false,
theme: 'light'
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
const fieldValue = type === 'checkbox' ? checked : value;
setSettings(prev => ({ ...prev, [name]: fieldValue }));
console.log(name + ':', fieldValue);
};
// Simulate different input types
handleChange({ target: { name: 'username', value: 'bob', type: 'text', checked: false } });
handleChange({ target: { name: 'notifications', value: '', type: 'checkbox', checked: true } });
handleChange({ target: { name: 'theme', value: 'dark', type: 'select-one', checked: false } });
return null;
}
SettingsForm();
The handler checks the input type. For checkboxes it uses event.target.checked, for everything else it uses event.target.value. This single handler works with text inputs, checkboxes, selects, and more.
const initialValues = {
firstName: 'John',
lastName: 'Doe',
email: 'john@test.com'
};
function EditProfile() {
const [form, setForm] = React.useState(initialValues);
const isDirty = JSON.stringify(form) !== JSON.stringify(initialValues);
const handleReset = () => {
setForm(initialValues);
console.log('Form reset to initial values');
};
console.log('Is dirty:', isDirty);
// Simulate editing
setForm(prev => ({ ...prev, firstName: 'Jane' }));
const afterEdit = { ...form, firstName: 'Jane' };
const dirtyAfterEdit = JSON.stringify(afterEdit) !== JSON.stringify(initialValues);
console.log('After edit, is dirty:', dirtyAfterEdit);
// Reset
handleReset();
return null;
}
EditProfile();
Using a single state object makes dirty checking straightforward - compare current state with initial values. Reset is also simple - just set state back to the initial object.
form state objects, computed property names, generic change handlers, complex forms, state management