Questions
This component's UI is not updating. Why is direct state mutation problematic?
A developer has built a simple TodoList component. It displays a list of tasks, and each task has a “Mark Complete” button. When the button is clicked, the task’s completed status should toggle, and the UI should update to reflect this change.
However, they’ve noticed a bug: clicking “Mark Complete” does not update the UI. The task’s status remains unchanged visually, even though the underlying data seems to be modified.
The Challenge
You’ve been given the TodoList component.
- Explain why directly mutating state (or objects within state) is problematic in React and why the UI is not updating.
- Fix the component to update the todo’s status immutably, ensuring the UI correctly reflects the changes.
import React, { useState } from 'react';
export default function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build a project', completed: false },
]);
const toggleComplete = (id) => {
const todo = todos.find(t => t.id === id);
if (todo) {
// THE BUG: Directly mutating the 'completed' property of the todo object.
// React's shallow comparison won't detect this change.
todo.completed = !todo.completed;
setTodos(todos); // Passing the same array reference
console.log(`Todo ${id} completed status: ${todo.completed}`);
}
};
return (
<div style={{ padding: '20px' }}>
<h1>Todo List</h1>
<ul>
{todos.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
<button onClick={() => toggleComplete(todo.id)} style={{ marginLeft: '10px' }}>
{todo.completed ? 'Undo' : 'Mark Complete'}
</button>
</li>
))}
</ul>
<p>Click "Mark Complete" and observe the UI.</p>
</div>
);
} The Solution
const toggleComplete = (id) => { const todo = todos.find(t => t.id === id); if (todo) { // THE BUG: Directly mutating the 'completed' property of the todo object. // React's shallow comparison won't detect this change. todo.completed = !todo.completed; setTodos(todos); // Passing the same array reference console.log(`Todo ${id} completed status: ${todo.completed}`); } };
const toggleComplete = (id) => { // FIX: Update state immutably by creating new objects/arrays. setTodos(prevTodos => prevTodos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } // Create a NEW todo object : todo ) ); };
Why This Works
The problem arises because React relies on immutability for efficient change detection during its reconciliation process.
When you update state in React (e.g., by calling setTodos), React performs a shallow comparison between the new state value and the previous state value.
- If the new state value is a primitive (number, string, boolean) and is different, React knows to re-render.
- If the new state value is an object or an array, React only compares their references. If the reference is the same, React assumes the content hasn’t changed and does not trigger a re-render.
In the buggy code:
todo.completed = !todo.completed;directly mutates thetodoobject within thetodosarray.setTodos(todos);is then called. However, thetodosarray itself is the same array instance as before. Its reference hasn’t changed.
Because the todos array’s reference remains the same, React’s shallow comparison doesn’t detect a change in the todos state, and therefore, the TodoList component does not re-render, leading to a stale UI.
The Fix: Updating State Immutably
To correctly update state that contains objects or arrays, you must always create a new object or array with the desired changes. This ensures that React detects a new reference and triggers a re-render.
Here’s how to update the todos array immutably:
- Map over the
todosarray. - When you find the
todoto update, create a newtodoobject with the updated properties (using spread syntax...). - Return a new array containing the updated
todoand the other unchangedtodos.
By creating a new todo object and a new todos array, React now detects a change in the todos state’s reference, triggering a re-render and correctly updating the UI.
Practice Question
What is the primary consequence of directly mutating a state object or array in React?