DeployU
Interviews / Frontend Engineering / This component's UI is not updating. Why is direct state mutation problematic?

Questions

This component's UI is not updating. Why is direct state mutation problematic?

debugging State Management Interactive Quiz Code Examples

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.

  1. Explain why directly mutating state (or objects within state) is problematic in React and why the UI is not updating.
  2. Fix the component to update the todo’s status immutably, ensuring the UI correctly reflects the changes.
Your Code
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>
  );
}
Click "Run Code" to see output...
Click "Run Code" to see test results...

The Solution

Wrong Approach

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}`); } };

Right Approach

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:

  1. todo.completed = !todo.completed; directly mutates the todo object within the todos array.
  2. setTodos(todos); is then called. However, the todos array 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:

  1. Map over the todos array.
  2. When you find the todo to update, create a new todo object with the updated properties (using spread syntax ...).
  3. Return a new array containing the updated todo and the other unchanged todos.

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?