- Published at
How Not to Update the State in React
Table of Contents
If you are a beginner to React, you have almost certainly run into this scenario: you trigger a function to update your state, but when you look at your running app, the UI hasn’t changed. You add console.log statements, and you can see the data changing under the hood, but the screen stubbornly refuses to reflect it.
Let’s look at why this happens and how to fix it.
Changing State
If you are working with primitive types (like strings, numbers, and booleans), then updating the state is straightforward:
const [age, setAge] = useState(24);
function handleBirthday() {
setAge(25);
}
When handleBirthday() is invoked, the age state variable changes from 24 to 25. React notices this change and triggers a re-render to update the UI. But what happens when our state is an array or an object?
Now consider an object in state:
const [userProfile, setUserProfile] = useState({ name: 'Alice', age: 24 });
Technically, it is possible to change the contents of the object itself. This is called a mutation:
userProfile.age = 25;
But if you tried to update the state by changing the object, you will notice that React doesn’t rerender the UI:
const [userProfile, setUserProfile] = useState({ name: 'Alice', age: 24 });
function handleBirthday() {
userProfile.age = 25;
setUserProfile(userProfile);
}
To understand why this happens, let’s take a quick detour into how JavaScript handles memory.
The Root of the Problem: JavaScript Data Types
In JavaScript, data types are divided into two main categories, and they behave very differently in memory:
| Category | Types | Characteristic | Memory Behavior |
|---|---|---|---|
| Primitives | Number, String, Boolean, null, undefined, BigInt, Symbol | Immutable values | Variables hold the actual value. Reassigning points the variable to an entirely new memory location. |
| References | Object, Array, Function | Mutable (Changeable) | Variables hold a reference (a pointer) to a location in memory. |
Immutability Explained
When you appear to “change” an integer variable, you are actually performing a reassignment, not a mutation of the original value.
let x = 5;
x = 10;
In this code, a memory location is created to hold the value 5. When you execute x = 10;, a new memory location is created for the value 10, and x is updated to point there. The original value of 5 remains completely unchanged in memory. Primitive values in JavaScript are immutable. When you appear to change them, you are actually reassigning the variable to a new value.
Mutability Explained
This differs entirely from mutable types like objects and arrays:
const obj = { value: 5 };
obj.value = 10;
Here, a single object is created in memory, and the obj variable holds a reference (pointer) to it. When obj.value is updated, the content inside the existing memory location is modified. The obj variable still points to the exact same memory address.
How React Detects Changes
Calling a state setter like setAge tells React that the state might have changed. React then compares the new value with the previous state value. Internally, React uses Object.is for this comparison, which behaves similarly to === for most cases. If the two values are considered equal, React assumes nothing has changed and may skip scheduling a re-render. For objects and arrays, this effectively becomes a reference equality check, since the comparison only returns true when both values point to the same object in memory.
With the below example, when the value of age changes, React sees a new primitive value and re-renders.
const [age, setAge] = useState(24);
function handleBirthday() {
setAge(25); // React sees a new primitive value and re-renders
}
However, if you mutate an object or an array directly and pass it back into a state setter function, React looks at the memory reference. Because the reference hasn’t changed (it’s the exact same object in memory, just with different contents), React assumes no update is required and bails out of scheduling a re-render.
In the below code, when setUserProfile() is called and passed the modified object, React sees the exact same reference and assumes there is no change since it is dealing with the same object, and so it doesn’t rerender.
const [userProfile, setUserProfile] = useState({ name: 'Alice', age: 24 });
function handleBirthday() {
// BAD: Mutating the object directly
userProfile.age = 25;
// React sees the exact same memory reference and bails out of the re-render
setUserProfile(userProfile);
}
The same is true for arrays:
const [colors, setColors] = useState(['red', 'green', 'blue']);
const changeColor = () => {
// BAD: Mutating the array directly
colors[0] = 'yellow';
// React sees the exact same memory reference and bails out of the re-render
setColors(colors);
};
In React, although objects in state are technically mutable, you should treat them as if they were immutable — like numbers, booleans, and strings. Instead of mutating them, you should always replace them.
The golden rule of React: Treat all state as if it were immutable. Never mutate state directly.
How to Update State Correctly
Instead of mutating the existing object or array, you must create a brand new copy with the desired changes, and pass that new copy to the state setter function. Because it is a new copy, it has a new memory reference, and React will process the re-render.
Updating Arrays in State
Do not use array methods that modify the original array in place (like push, pop, or splice). Instead, use non-mutating methods like map(), filter(), concat(), or the array spread syntax (...).
BAD: Mutating the array
const [names, setNames] = useState(['john', 'jane']);
// This modifies the existing array. React won't re-render!
names.push('stacy');
setNames(names);
GOOD: Creating a new array
const [names, setNames] = useState(['john', 'jane']);
// Creates a new array, copies old elements, and adds the new one
setNames([...names, 'stacy']);
Updating Objects in State
Similarly, do not modify object properties directly. Use the object spread syntax (...) to create a new object, copying existing properties and overwriting only the ones you need to change.
BAD: Mutating the object
const [fruit, setFruit] = useState({ color: 'green', name: 'banana' });
// This modifies the property in place. React won't re-render!
fruit.color = 'yellow';
setFruit(fruit);
GOOD: Creating a new object
const [fruit, setFruit] = useState({ color: 'green', name: 'banana' });
// Creates a new object, copies old properties, and overwrites 'color'
setFruit({ ...fruit, color: 'yellow' });
A Note on Nested Objects
For nested objects, the spread operator only copies the first level. The spread operator creates a shallow copy — it only duplicates the top-level keys, while nested objects still share the same memory reference. You must explicitly spread each level down to the point of the change you want to make.
For example, if you have the following:
const [user, setUser] = useState({
name: 'Bob',
email: 'bob@example.com',
settings: {
theme: 'light',
receiveEmails: true,
},
});
To toggle the theme to Dark Mode, you might do:
BAD: Forgetting to copy the nested object
function toggleTheme() {
setUser((prevUser) => ({
...prevUser, // Copies name and email
settings: {
theme: 'dark', // OVERWRITES the entire settings object!
},
}));
}
In the above, Bob gets dark mode, but the receiveEmails property is completely wiped out. By creating a new settings object and only passing in theme, React replaces the old settings object entirely. Bob will never get another email.
GOOD: Spreading at every level
To do this correctly, you must spread the top-level object and spread the nested object before making your specific change.
function toggleTheme() {
setUser((prevUser) => ({
...prevUser, // 1. Copy top-level properties (name, email)
settings: {
...prevUser.settings, // 2. Copy nested properties (theme, receiveEmails)
theme: 'dark', // 3. Overwrite only the theme
},
}));
}
Conclusion
By following the principle of immutability, you give React exactly what it needs to manage state effectively, resulting in snappy, predictable UI updates and a bug-free app.