Object State
Overview
Often, related pieces of data (like a user's firstName, lastName, and email) should be grouped together into a single JavaScript object in State, rather than creating 10 different useState variables.
However, storing objects in state introduces a massive challenge: State must be treated as Immutable.
You are strictly forbidden from directly modifying an object's properties in React (e.g., user.firstName = 'Bob'). If you mutate the object directly, the object's underlying memory reference address never changes. React does a shallow comparison, sees that the memory address is identical, assumes nothing changed, and completely ignores the update. The UI will not re-render.
To update an object, you MUST create a brand new object in memory, copy over all the old data using the Spread Operator (...), and overwrite the specific fields you want to change.
Syntax
function ProfileSettings() {
const [user, setUser] = useState({
name: "Alice",
age: 25,
role: "Admin"
});
const updateName = (newName) => {
// BAD: Mutation (UI will NOT update)
// user.name = newName;
// setUser(user);
// GOOD: Creating a brand new object
setUser({
...user, // 1. Copy everything from the old object (age, role)
name: newName // 2. Overwrite the 'name' property
});
};
return <button onClick={() => updateName('Kartik')}>Change Name</button>;
}Common Pitfalls
- Forgetting the spread operator: If you call
setUser({ name: 'Kartik' }), you completely overwrite the entire object! The new state will ONLY have anameproperty. Theageandroleproperties will be permanently deleted. You must always spread the old state first. - Nested Objects require Nested Spreads: The spread operator
...only creates a shallow copy. If your state object has a nested object (e.g.,user.address.city), you must spread the parent object AND spread the nested object to safely update it.
Interview Questions
React optimizes performance by comparing the old state and new state using strict equality (oldState === newState), which checks memory references. If you mutate an object, the memory reference remains the same. React thinks no change occurred and aborts the render cycle.
Updating deeply nested objects with raw spread operators gets incredibly messy. In enterprise apps, developers commonly use a library called Immer. Immer allows you to write mutating syntax (draft.user.address.city = 'NY') and secretly converts it into perfect immutable spread operations under the hood.
Real-World Example
Handling Multiple Form Inputs: By storing the form as an object and using the name attribute of the input, we can write a single elegant handleChange function that dynamically updates whichever field the user is currently typing in.
function RegistrationForm() {
// Grouping the entire form into a single object
const [formData, setFormData] = useState({
username: "",
email: "",
password: ""
});
// ONE master change handler for all inputs!
const handleChange = (e) => {
// Dynamic key assignment using e.target.name
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
return (
<form>
<input
name="username"
value={formData.username}
onChange={handleChange}
/>
<input
name="email"
value={formData.email}
onChange={handleChange}
/>
</form>
);
}Check Your Knowledge
Test your understanding of Object State with these quick questions.