Immutability in State Management (React & Redux)

Ashik Rahman - Feb 9 - - Dev Community

Immutability plays a crucial role in managing state efficiently, especially in libraries like React and Redux. It helps track state changes, optimize rendering, and prevent unintended mutations.


1. Immutability in React (useState)

React’s state should be treated as immutable to ensure proper re-renders.

Example (Mutating State - Wrong Approach)

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    count++;  // ❌ Directly modifying state (WILL NOT trigger re-render)
    setCount(count);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why is this wrong?

  • Directly modifying count does not create a new reference.
  • React doesn't detect a change and does not re-render.

Correct Approach (Immutable Update)

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);  // ✅ Creates a new value, triggering a re-render
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, React re-renders because setCount returns a new value.


2. Immutability in Redux (Reducers & State Updates)

Redux enforces immutability to track changes properly.

Wrong Approach (Mutating Redux State)

const initialState = { count: 0 };

function counterReducer(state = initialState, action) {
  if (action.type === "INCREMENT") {
    state.count++; // ❌ Directly modifying state (BAD PRACTICE)
    return state;
  }
  return state;
}
Enter fullscreen mode Exit fullscreen mode

Why is this bad?

  • Redux relies on detecting new state objects.
  • Direct mutation does not create a new object, so Redux doesn’t detect the change.

Correct Approach (Immutable Redux Update)

const initialState = { count: 0 };

function counterReducer(state = initialState, action) {
  if (action.type === "INCREMENT") {
    return { ...state, count: state.count + 1 }; // ✅ Creating a new state object
  }
  return state;
}
Enter fullscreen mode Exit fullscreen mode

Now, Redux detects the change and updates the UI.


3. Immutability in Arrays (React & Redux)

Wrong Approach (Mutating an Array in State)

const [todos, setTodos] = useState(["Learn React"]);

const addTodo = () => {
  todos.push("Learn Redux"); // ❌ Directly modifying array
  setTodos(todos); // ❌ State doesn't change reference, React won't re-render
};
Enter fullscreen mode Exit fullscreen mode

Correct Approach (Immutable Update with Spread Operator)

const [todos, setTodos] = useState(["Learn React"]);

const addTodo = () => {
  setTodos([...todos, "Learn Redux"]); // ✅ Creates a new array
};
Enter fullscreen mode Exit fullscreen mode

✅ Now React detects the state change and re-renders.


4. Using Immer.js for Immutability

Instead of manually handling immutability, you can use Immer.js, which lets you write code as if it's mutable but keeps it immutable.

Example with Immer.js

import produce from "immer";

const initialState = { count: 0 };

function counterReducer(state = initialState, action) {
  return produce(state, (draft) => {
    if (action.type === "INCREMENT") {
      draft.count++; // ✅ Looks like mutation, but it's immutable
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

Immer handles immutability automatically while keeping the code simple.


Key Takeaways

React state updates should be immutable (use setState with new objects or arrays).

Redux state should never be mutated (always return a new object in reducers).

Use spread operator (...) for objects & arrays to ensure immutability.

Immer.js simplifies immutability in Redux and complex state updates.

. . . . . . . .