React is one of the most popular JavaScript libraries for building user interfaces. But sometimes, it can be tricky to understand why changes you make to state don't always seem to update what you see on the screen. In this blog, we'll explore why changing the state using an object might not cause a component to rerender. I'll keep it simple, so even if you're just starting with React, you'll be able to follow along.
React State and Rerendering
React uses state to determine what a component should look like. When you change the state, React automatically updates, or rerenders, the component to match the new state. But there's a catch: React has some rules about what kinds of changes it notices.
If you update the state in a way that React doesn't detect, your component won't rerender. This can lead to situations where you expect the UI to update, but nothing happens.
The Problem with Changing State with an Object
Updating State in the Wrong Way
In React, when we use objects in our state, we might accidentally run into an issue where changes we make don't cause a rerender. This usually happens because of how references work in JavaScript. Let me explain with a simple example:
const [person, setPerson] = useState({ name: 'Alice', age: 25 }); // Updating the age like this won't rerender the component person.age = 26; setPerson(person);
At first glance, it looks like we updated the age of our person
object and then used setPerson
to update the state. But when you run this code, the component doesn't rerender! Why?
React Needs a New Reference
React uses something called referential equality to decide if the state has changed. In simple terms, React checks if the memory location of the state has changed. In the above example, we modified a property of the person
object, but the reference to the person
object is still the same. React thinks nothing significant has changed, so it decides not to rerender the component.
How to Fix It
The solution is to create a new object when updating the state. Instead of modifying the existing person
object, we create a new one that includes the updated value:
setPerson({ ...person, age: 26 });
In this line, { ...person, age: 26 }
creates a new object that copies all the properties from person
but with an updated age
. Since this is a brand new object, React sees that the reference has changed and rerenders the component.
Why React Does This
React's way of checking if it needs to rerender is designed to be fast. If React had to deeply compare every property of every object, it would slow down your app. Instead, it just checks if the reference to the object is different. If it's different, React assumes something has changed and rerenders the component.
Common Mistakes When Updating State
Mutating State Directly
One common mistake is to mutate (change) the state directly, especially with objects or arrays. Direct mutations can lead to unpredictable behavior because React might not detect the change. Here’s an example with an array:
const [items, setItems] = useState([1, 2, 3]); // Adding an item like this won't rerender the component items.push(4); setItems(items);
In this example, we're directly modifying the items
array using push()
, which means the reference stays the same. To fix it, we need to create a new array:
setItems([...items, 4]);
This way, React can tell that the items
array has a new reference and rerender accordingly.
Best Practices for Updating State in React
Always Create a New Copy
Whenever you update an object or an array in React, always create a new copy. You can use the spread operator (...
) for this, as we did in the examples above. For arrays, methods like map()
, filter()
, and concat()
are also helpful because they return new arrays.
Avoid Mutations
Mutating objects or arrays directly can lead to bugs that are hard to track down. Instead, always make sure you're creating a new version of your state. This will help React properly detect changes and keep your UI in sync.
Use Functional Updates When Necessary
Sometimes, when you need to update state based on its previous value, use a functional update to avoid potential issues:
setPerson(prevPerson => ({ ...prevPerson, age: prevPerson.age + 1 }));
This ensures that you're always working with the latest version of the state.