DEV Community

Jason M
Jason M

Posted on

Ensuring Immutability in Redux Reducers: Tips for Beginners

The front-end for web applications has become increasingly complex, and each year there seem to be more sophisticated data structures, more variables, and more dependencies. While this complexity provides a richer user experience, it can lead to a headache in terms of maintenance and debugging for developers.

Fortunately, along with the difficulties increased complexity has brought us, we've been given a few wonderful tools to deal with it. One of these tools is called Redux. Redux allows us to manage the data which underlies our web application's UI in an effective manner by (1) making sure that data lives in one location, and (2) enforcing a single process for updating that data.

In Redux, the data lives in the store, a plain javascript object that can take many forms, depending on the different kinds of data your application uses. Here's a simple example:


const store = {
  authors: [
    {
      name: "Robert Jordan",
      id: "1",
      books: ["16"]
    },

    {
      name: "Stephen King",
      id: "6",
      books: ["2"]
    }
  ],

  books: [
    {
      name: "the eye of the world",
      id: "16",
      authors: ["1"]
    },

    {
      name: "the shining",
      id: "2",
      authors: ["6"]
    }
  ]
};    

In the above example, the store has two kinds of data for the application, books and authors. In the Redux system, each of these pieces of data will have a separate Reducer function which is responsible for handling updates for that piece of data. The reducer for books might look a little bit like this:


import { ADD_BOOK, DELETE_BOOK } from "../actions/index.js";

const booksReducer = (state = [], action) => {
  switch (action.type) {
    case ADD_BOOK:
      return [...state, action.payload];

    case DELETE_BOOK:
      return state.filter(book => book.id != action.payload);

    default:
      return state;
  }
};

export default booksReducer;


Any update that is going to happen to the books array in the store will be done through this booksReducer function. This reducer takes the existing state (in this case, the books array), and a redux action object as arguments. The action object might look like this:


{
    type: ADD_BOOK,
    payload: {
     id: 7,
     name: "Robin Hobb",
     books: ["456", "27"] 
    }   
}

The action object tells the reducer function what sort of thing to do with the accompanying payload information. So, this action will be used to update the store with the new book, in the switch statement for the ADD_BOOK case.

However, whatever action happens to be passed to a reducer function, it's incredibly important that any work done inside the function does not mutate the original state, but instead returns a new altered copy of the data. Violating this principle could very well lead you down a horrible rabbit hole of debugging.

But there are a number of options available to decrease or eliminate the likelihood of accidentally causing mutations in your data inside your reducer functions.

  1. For each of your reducers, for each case or possibility, write a test to make sure no mutations have occurred.

The easiest way to structure this test is just to use a deep-copy library, to copy your state before applying the reducer function to the original state. Then, call the reducer function, and using deep comparison testing (which most test frameworks, such as mocha, will have), make sure the returned state is deeply equal to the original state.

Using some simple tests in this way, you can find out if any of your reducer functions are slipping some unexpected mutations.

  1. Deep-copy your state inside the reducer function before doing any work on it. Then return the altered copy.

  2. Use a library like Immer or Immutable that has custom, immutable objects which don't let you mutate state.

Option 1 is always a good idea, since it can never hurt to have a few more tests, and the time writing the tests could very well save you a lot more time debugging code in the future. Option 2 will ensure that no mutation occurs, but straight up deep copying before doing any work in the reducer is wasteful from a performance perspective, so should be avoided on larger projects.

Option 3 will ensure immutability, but it will also require you to learn another library, and introduce another dependency into your project. Some of these libraries are easier to work with than others, so if you're going to take this route, I suggest reading up the tradeoffs between the popular immutable libraries like Immer and Immutable.

Top comments (0)