Concept of Immutability and How To Write Immutable Code

Immutability can be a confusing topic, and it pops up all over the place in React, Redux, and JavaScript in general.

What is Immutability?
First off, immutable is the opposite of mutable – and mutable means changeable, modifiable… able to be messed with. So something that is immutable, then, is something that cannot be changed.

While JavaScript isn’t a purely functional language, it can sorta pretend to be sometimes. Certain Array operations in JS are immutable (meaning that they return a new array, instead of modifying the original). String operations are always immutable (they create a new string with the changes).

Rules of Immutability
In order to be pure a function must follow these rules:
- A pure function must always return the same value when given the same inputs.
- A Pure functions can only call other pure functions.
- A pure function must not have any side effects.

JS Array Methods That Mutate
Certain array methods will mutate the array they’re used on:
- push (add an item to the end)
- pop (remove an item from the end)
- shift (remove an item from the beginning)
- unshift (add an item to the beginning)
- sort
- reverse
- splice

Immutable JS Methods (Instead of modifying array, it creates an entirely new one)
- The ... Spread Operator
- slice
- concat
- Object.assign

How To Update State in Redux
Redux requires that its reducers be pure functions. This means you can’t modify the state directly – you have to create a new state based on the old one. Writing code to do immutable state updates can be tricky.

Below examples make heavy use of the spread operator for arrays and objects. When this ... notation is placed before an object or array, it unwraps the children within, and inserts them right there. The spread operator makes it easy to create a new object or array that contains the exact same contents as another one. This is useful for creating a copy of an object/array, and then overwriting specific properties that you need to change.

These examples are written in the context of returning state from a Redux reducer. I’ll show what the incoming state looks like, and then show how to return an updated state. For the sake of keeping the examples clean, I’m gonna ignore the “action” parameter entirely. Pretend that this state update will happen for any action.

These examples will show what immutability is and how to write immutable code in your own apps.
- Update an Object
- Update an Object in an Object
- Updating an Object by Key
- Prepend an item to an array
- Add an item to an array
- Insert an item in the middle of an array
- Update an item in an array by index
- Update an item in an array with map
- Update an object in an array
- Remove an item from an array with filter

Update an Object
When you want to update the top-level properties in the Redux state object, copy the existing state with ...state and then list out the properties you want to change, with their new values.
function reducer(state, action) {
  /*
    State looks like:

    state = {
      clicks: 0,
      count: 0
    }
  */

  return {
    ...state,
    clicks: state.clicks + 1,
    count: state.count - 1
  }
}

Update an Object in an Object
When the object you want to update is one (or more) levels deep within the Redux state, you need to make a copy of every level up to and including the object you want to update. Here’s an example one level deep:
function reducer(state, action) {
  /*
    State looks like:

    state = {
      house: {
        name: "Ravenclaw",
        points: 17
      }
    }
  */

  // Two points for Ravenclaw
  return {
    ...state, // copy the state (level 0)
    house: {
      ...state.house, // copy the nested object (level 1)
      points: state.house.points + 2
    }
  }
Here’s another example, this time updating an object that’s two levels deep:
function reducer(state, action) {
  /*
    State looks like:

    state = {
      school: {
        name: "Hogwarts",
        house: {
          name: "Ravenclaw",
          points: 17
        }
      }
    }
  */

  // Two points for Ravenclaw
  return {
    ...state, // copy the state (level 0)
    school: {
      ...state.school, // copy level 1
      house: {         // replace state.school.house...
        ...state.school.house, // copy existing house properties
        points: state.school.house.points + 2  // change a property
      }
    }
  }

Updating an Object by Key
function reducer(state, action) {
  /*
    State looks like:

    const state = {
      houses: {
        gryffindor: {
          points: 15
        },
        ravenclaw: {
          points: 18
        },
        hufflepuff: {
          points: 7
        },
        slytherin: {
          points: 5
        }
      }
    }
  */

  // Add 3 points to Ravenclaw,
  // when the name is stored in a variable
  const key = "ravenclaw";
  return {
    ...state, // copy state
    houses: {
      ...state.houses, // copy houses
      [key]: {  // update one specific house (using Computed Property syntax)
        ...state.houses[key],  // copy that specific house's properties
        points: state.houses[key].points + 3   // update its `points` property
      }
    }
  }

Prepend an item to an array
Here is how you can add an item to the beginning of an array in an immutable way, suitable for Redux:
function reducer(state, action) {
  /*
    State looks like:

    state = [1, 2, 3];
  */

  const newItem = 0;
  return [    // a new array
    newItem,  // add the new item first
    ...state  // then explode the old state at the end
  ];
}

Add an item to an array
Here is how you can append an item to the end of an array, immutably:
function reducer(state, action) {
  /*
    State looks like:

    state = [1, 2, 3];
  */

  const newItem = 0;
  return [    // a new array
    ...state, // explode the old state first
    newItem   // then add the new item at the end
  ];
You can also make a copy of the array with .slice, and then mutate the copy:
function reducer(state, action) {
  const newItem = 0;
  const newState = state.slice();

  newState.push(newItem);
  return newState;
}

Update an item in an array with map
Array’s .map function will return a new array by calling the function you provide, passing in each existing item, and using your return value as the new item’s value.

In other words, if you have an array with N many items and want a new array that still has N items, use .map. You can update/replace one or more items with a single pass through the array.
function reducer(state, action) {
  /*
    State looks like:

    state = [1, 2, "X", 4];
  */

  return state.map((item, index) => {
    // Replace "X" with 3
    // alternatively: you could look for a specific index
    if(item === "X") {
      return 3;
    }

    // Leave every other item unchanged
    return item;
  });
}

Update an object in an array
This works the same way as above. The only difference is we’ll need to construct a new object and return a copy of the one we want to change.

Array’s .map function will return a new array by calling the function you provide, passing in each existing item, and using your return value as the new item’s value.

In other words, if you have an array with N many items and want a new array that still has N items, use .map. You can update/replace one or more items with a single pass through the array.

In this example we have an array of users with email addresses. One of them changed their email and we need to update it. I’ll show how the user’s ID and new email could come in as part of the action, but you can adapt this to accept the values from somewhere else of course (if you’re not using Redux, for instance).
function reducer(state, action) {
  /*
    State looks like:

    state = [
      {
        id: 1,
        email: 'jen@reynholmindustries.com'
      },
      {
        id: 2,
        email: 'peter@initech.com'
      }
    ]

    Action contains the new info:

    action = {
      type: "UPDATE_EMAIL"
      payload: {
        userId: 2,  // Peter's ID
        newEmail: 'peter@construction.co'
      }
    }
  */

  return state.map((item, index) => {
    // Find the item with the matching id
    if(item.id === action.payload.userId) {
      // Return a new object
      return {
        ...item,  // copy the existing item
        email: action.payload.newEmail  // replace the email addr
      }
    }

    // Leave every other item unchanged
    return item;
  });
}

Insert an item in the middle of an array
Array’s .splice function will insert an item, but it will also mutate the array.

Since we don’t want to mutate the original, we can make a copy first (with .slice), then use .splice to insert an item into the copy.

The other way to do this involves copying in all the elements BEFORE the new one, then inserting the new one, and then copying in all the elements AFTER it. It’s easy to get the indices wrong though.
function reducer(state, action) {
  /*
    State looks like:

    state = [1, 2, 3, 5, 6];
  */

  const newItem = 4;

  // make a copy
  const newState = state.slice();

  // insert the new item at index 3
  newState.splice(3, 0, newItem)

  return newState;

  /*
  // You can also do it this way:

  return [                // make a new array
    ...state.slice(0, 3), // copy the first 3 items unchanged
    newItem,              // insert the new item
    ...state.slice(3)     // copy the rest, starting at index 3
  ];
  */
}

Update an item in an array by index
We can use Array’s .map to return a new value for a specific index, and leave the other elements unchanged.
function reducer(state, action) {
  /*
    State looks like:

    state = [1, 2, "X", 4];
  */

  return state.map((item, index) => {
    // Replace the item at index 2
    if(index === 2) {
      return 3;
    }

    // Leave every other item unchanged
    return item;
  });
}

Remove an item from an array with filter
Array’s .filter function will call the function you provide, pass in each existing item, and return a new array with only the items where your function returned "true". If you return false, that item gets removed.

If you have an array with N items and you want to end up with fewer items, use .filter.
function reducer(state, action) {
  /*
    State looks like:

    state = [1, 2, "X", 4];
  */

  return state.filter((item, index) => {
    // Remove item "X"
    // alternatively: you could look for a specific index
    if(item === "X") {
      return false;
    }

    // Every other item stays
    return true;
  });
}


For detail explanation and more, please visit Immutability in React and Redux: The Complete Guide by Dave Ceddia.

Comments

Popular Posts