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>
);
}
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>
);
}
✅ 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;
}
❌ 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;
}
✅ 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
};
Correct Approach (Immutable Update with Spread Operator)
const [todos, setTodos] = useState(["Learn React"]);
const addTodo = () => {
setTodos([...todos, "Learn Redux"]); // ✅ Creates a new array
};
✅ 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
}
});
}
✅ 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.