Open In App

How to handle Immutable State in Redux Reducers?

Last Updated : 29 Feb, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

Managing immutable states in reducers is a core principle of Redux that is used for maintaining the predictability and consistency of the application’s state throughout its lifespan. Immutable state denotes that once a state object is created, it cannot undergo direct modification. If there are any alterations to the state trigger, then there will be a fresh state object created.

In this article, we will see what is immutable state and different ways to handle the immutable states.

What is Immutable State?

Immutable state in Redux refers to the principle that the state within your Redux store should not be directly mutated. Instead, when you want to update the state, you create a new copy of the state with the desired changes. This ensures that the original state remains unchanged.

Benefits of Immutable state:

  • Predictability: With immutable state, you can easily predict how your application’s state will change over time since it avoids unexpected mutations.
  • Debugging: Immutable state makes it easier to debug your application because you can trace the changes to the state more accurately.
  • Performance Optimization: Immutable updates can be optimized for performance in certain scenarios, such as in React applications with shallow equality checks.
  • Time-Travel Debugging: Redux’s dev tools rely on immutability to enable features like time-travel debugging, where you can step backward and forward through the state changes.

Ways to Handle Immutable State

1. Return a New State Object

Reducers in Redux should always return a new state object, rather than mutating the existing state. This can be achieved by creating a new object and copying the existing state properties, then applying modifications as needed.

Example:

Javascript




const initialState = {
    counter: 0
};
 
const reducer = (state = initialState, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return {
                ...state,
                counter: state.counter + 1
            };
        case 'DECREMENT':
            return {
                ...state,
                counter: state.counter - 1
            };
        default:
            return state;
    }
};


2. Use Immutable Update Patterns

Immutable update patterns simplify the process of updating nested objects or arrays within the state. Techniques like the spread operator (`…`), `Object.assign()`, `Array.slice()`, and libraries like Immutable.js or Immer can be used to ensure immutability.

Example using Spread Operator:

Javascript




const initialState = {
    todos: []
};
 
const reducer = (state = initialState, action) => {
    switch (action.type) {
        case 'ADD_TODO':
            return {
                ...state,
                todos: [...state.todos, action.payload]
            };
        case 'REMOVE_TODO':
            return {
                ...state,
                todos: state.todos.filter(todo => todo.id !== action.payload.id)
            };
        default:
            return state;
    }
};


3. Avoid Mutating State Directly

It’s imperative that reducers refrain from directly mutating the state, whether it involves modifying properties or altering nested objects. Such actions violate the principle of immutability and can introduce unforeseen behavior and bugs into the application.

Example of Avoiding Mutation:

Javascript




// Incorrect approach - Mutating the state directly
const reducer = (state = initialState, action) => {
    switch (action.type) {
        case 'INCREMENT':
            state.counter++; // Mutating state directly
            return state;
        default:
            return state;
    }
};


Steps to Handle Immutability with Example:

Step 1: Create the application by running the following command.

npx create-react-app redux-immutable
cd redux-immutable

Step 2: Install the required dependencies

npm i redux react-redux

Folder Structure:

rgfh

Dependencies:

"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",
"react-scripts": "5.0.1",
"redux": "^5.0.1",
"web-vitals": "^2.1.4"
}

Example: Below is the provided code for different files.

Javascript




// App.js
 
import React from 'react';
import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';
import reducer from './reducer';
import { increment, changeValue } from './actions';
 
const App = ({ counter, nestedValue, increment, changeValue }) => {
    return (
        <div>
            <h1>Counter: {counter}</h1>
            <button onClick={increment}>Increment</button>
            <h2>Nested Object Value: {nestedValue}</h2>
            <button onClick={() =>
                changeValue('New Value')}>
                    Change Value</button>
        </div>
    );
};
 
const mapStateToProps = (state) => ({
    counter: state.counter,
    nestedValue: state.nestedObject.value
});
 
const mapDispatchToProps = {
    increment,
    changeValue
};
 
const ConnectedApp = connect(mapStateToProps, mapDispatchToProps)(App);
 
const store = createStore(reducer);
 
const ReduxApp = () => {
    return (
        <Provider store={store}>
            <ConnectedApp />
        </Provider>
    );
};
 
export default ReduxApp;


Javascript




// reducer.js
 
const initialState = {
    counter: 0,
    nestedObject: {
        value: 'Hello'
    }
};
 
const reducer = (state = initialState, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return {
                ...state,
                counter: state.counter + 1
            };
        case 'CHANGE_VALUE':
            return {
                ...state,
                nestedObject: {
                    ...state.nestedObject,
                    value: action.payload
                }
            };
        default:
            return state;
    }
};
 
export default reducer;


Javascript




// actions.js
 
export const increment = () => ({
    type: 'INCREMENT'
});
export const changeValue = (newValue) => ({
    type: 'CHANGE_VALUE', payload: newValue
});


To start the application run the following command:

npm start

Output:

Untitled-design-(1)

Output



Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads