Sign in
Log inSign up
Immutability in React and Redux: The Complete Guide

Immutability in React and Redux: The Complete Guide

Dave Ceddia's photo
Dave Ceddia
·Oct 8, 2018

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

You might’ve run into bugs where your React components don’t re-render, even though you know you’ve changed the props, and someone said, “You should be doing immutable state updates.” Maybe you or one of your teammates regularly writes Redux reducers that mutate state, and you have to constantly correct them (the reducers, or your teammates 😄)

It’s tricky. It can be really subtle, especially if you’re not sure what to look for. And honestly, if you’re not sure why it matters, it’s hard to care.

This guide will explain what immutability is and how to write immutable code in your own apps. Here’s what we’ll cover:

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.

Taken to an extreme, this means that instead of having traditional variables, you’d be constantly creating new values and replacing old ones. JavaScript is not this extreme, but some languages don’t allow mutation at all (Elixir, Erlang, and ML come to mind).

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). And you can write your own functions that are immutable, too. You just need to be aware of a few rules.

A Code Example with Mutation

Let’s look at an example to see how mutability works. We’ll start with this person object here:

let person = {
    firstName: "Bob",
    lastName: "Loblaw",
    address: {
        street: "123 Fake St",
        city: "Emberton",
        state: "NJ"
    }
}

Then let’s say we write a function that gives a person special powers:

function giveAwesomePowers(person) {
    person.specialPower = "invisibility";
    return person;
}

Ok so everyone gets the same power. Whatever, invisibility is great.

Let’s give some special powers to Mr. Loblaw now.

// Initially, Bob has no powers :(
console.log(person);

// Then we call our function...
let samePerson = giveAwesomePowers(person);

// Now Bob has powers!
console.log(person);
console.log(samePerson);

// He's the same person in every other respect, though.
console.log('Are they the same?', person === samePerson); // true

This function giveAwesomePowers mutates the person passed into it. Run this code, and you’ll see that the first time we print out person, Bob has no specialPower property. But then, the second time, he suddenly has the specialPower of invisibility.

The thing is, since this function modified the person that was passed in, we don’t know what the old one looked like anymore. They are forever changed.

The object returned from giveAwesomePowers is the same object as the one that was passed in, but its insides have been messed with. Its properties have changed. It has been mutated.

I want to say this again because it’s important: the internals of the object have changed, but the object reference has not. It’s the same object on the outside (which is why an equality check like person === samePerson will be true).

If we want the giveAwesomePowers function not to modify the person, we’ll have to make a few changes. First, though, let’s look at what makes a function pure, because it’s very closely related to immutability.

Rules of Immutability

In order to be pure a function must follow these rules:

  1. A pure function must always return the same value when given the same inputs.
  2. A pure function must not have any side effects.

What’s a “Side Effect”?

“Side effects” is a broad term, but basically, it means modifying things outside the scope of that immediate function. Some examples of side effects…

  • Mutating/modifying input parameters, like giveAwesomePowers does
  • Modifying any other state outside the function, like global variables, or document.(anything) or window.(anything)
  • Making API calls
  • console.log()
  • Math.random()

The API calls one might surprise you. After all, making a call to something like fetch('/users') might not appear to change anything in your UI at all.

But ask yourself this: If you called fetch('/users'), could it change anything anywhere? Even outside your UI?

Yep. It’ll create an entry in the browser’s Network log. It’ll create (and maybe later shut down) a network connection to the server. And once that call hits the server, all bets are off. The server could do whatever it wants, including calling out to other services and making more mutations. At the very least, it’ll probably put an entry in a log file somewhere (which is a mutation).

So, like I said: “side effect” is a pretty broad term. Here’s a function that has no side effects:

function add(a, b) {
  return a + b;
}

You can call this once, you can call it a million times, and nothing else in the world will change. I mean, technically, things in the world might change while the function runs. Time will pass… empires may fall… but calling this function will not directly cause any of those things. That satisfies Rule 2 – no side effects.

What’s more, every time you call this function like add(1, 2) you will get the same answer. No matter how many times you call add(1, 2) you will get the same answer. That satisifies Rule 1 – same inputs == same answers.

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

Yep, JS Array’s sort is not immutable! It will sort the array in place. The easiest thing to do, if you need to use one of these operations, is to make a copy of the array and then operate on the copy. You can copy an array with any of these methods:

let a = [1, 2, 3];
let copy1 = [...a];
let copy2 = a.slice();
let copy3 = a.concat();

So, if you wanted to do an immutable sort on an array, you could do it like this:

let sortedArray = [...originalArray].sort(compareFunction);

And one quick aside about sort (which has bitten me in the past) is that the compareFunction needs to return 0, 1, or -1. Not a boolean! Keep that in mind next time you’re writing a comparator.

Pure Functions Can Only Call Other Pure Functions

One potential source of trouble is calling a non-pure function from a pure one.

Purity is transititive, and it’s all-or-nothing. You can write a perfect pure function, but if you end it with a call to some other function that eventually calls setState or dispatch or causes some other sort of side effect… then all bets are off.

Now, there are some sorts of side effects that are “acceptable.” Logging messages with console.log is fine. Yeah, it’s technically a side effect, but it’s not going to affect anything.

A Pure Version of giveAwesomePowers

Now we can rewrite our function with the Rules in mind.

function giveAwesomePowers(person) {
  let newPerson = Object.assign({}, person, {
    specialPower: 'invisibility'
  })

  return newPerson;
}

This is a bit different now. Instead of modifying the person, we’re creating an entirely new person.

If you haven’t seen Object.assign, what it does is assign properties from one object to another. You can pass it a series of objects, and it will merge them all together, left to right, while overwriting any duplicate properties. (And by “left to right”, I mean that executing Object.assign(result, a, b, c) will copy a into result, then b, then c).

It doesn’t do a deep merge though – only the immediate child properties of each argument will be moved over. It also, importantly, does not create copies or clones of the properties. It assigns them as-is, keeping references intact.

So the code above creates an empty object, then assigns all of person’s properties to that empty object, and then assigns the specialPower property to that object as well. Another way to write this is with the object spread operator:

function giveAwesomePowers(person) {
  let newPerson = {
    ...person,
    specialPower: 'invisibility'
  }

  return newPerson;
}

You can read this as: “Create a new object, then insert the properties from person, then add another property called specialPower”. As of this writing, this object spread syntax is officially part of the JavaScript spec in ES2018.

Pure Functions Return Brand New Objects

Now we can re-run our experiment from earlier, using our new pure version of giveAwesomePowers.

// Initially, Bob has no powers :(
console.log(person);

// Then we call our function...
var newPerson = giveAwesomePowers(person);

// Now Bob's clone has powers!
console.log(person);
console.log(newPerson);

// The newPerson is a clone
console.log('Are they the same?', person === newPerson); // false

The big difference is that person was not modified. Bob is unchanged. The function created a clone of Bob, with all the same properties, plus the ability to go invisible.

This is sort of a weird thing about functional programming. Objects are constantly being created and destroyed. We do not change Bob; we create a clone, modify his clone, and then replace Bob with his clone. A bit grim, really. If you’ve seen that movie The Prestige, it’s kind of like that. (If you haven’t seen it, forget I said anything.)

React Prefers Immutability

In React’s case, it’s important to never mutate state or props. Whether a component is a function or a class doesn’t matter for this rule. If you’re about to write code like this.state.something = ... or this.props.something = ..., take a step back and try to come up with a better way.

To modify state, always use this.setState. If you’re curious you can read more about why not to modify state directly.

As for props, they’re a one-way thing. Props come IN to a component. They’re not a two-way street, at least not via mutable operations like setting a prop to a new value.

If you need to send some data back to the parent, or trigger something in the parent component, you can do that by passing in a function as a prop, and then calling that function from inside the child whenever you need to communicate to the parent. Here’s a quick example of a callback prop that works this way:

function Child(props) {
  // When the button is clicked,
  // it calls the function that Parent passed down.
  return (
    <button onClick={props.printMessage}>
      Click Me
    </button>
  );
}

function Parent() {
  function printMessage() {
    console.log('you clicked the button');
  }

  // Parent passes a function to Child as a prop
  // Note: it passes the function name, not the result of
  // calling it. It's printMessage, not printMessage()
  return (
    <Child onClick={printMessage} />
  );
}

Immutability Is Important for PureComponents

By default, React components (both the function type and the class type, if it extends React.Component) will re-render whenever their parent re-renders, or whenever you change their state with setState.

An easy way to optimize a React component for performance is to make it a class, and make it extend React.PureComponent instead of React.Component. This way, the component will only re-render if it’s state is changed or if it’s props have changed. It will no longer mindlessly re-render every single time its parent re-renders; it will ONLY re-render if one of its props has changed since the last render.

Here’s where immutability comes in: if you’re passing props into a PureComponent, you have to make sure that those props are updated in an immutable way. That means, if they’re objects or arrays, you’ve gotta replace the entire value with a new (modified) object or array. Just like with Bob – kill it off and replace it with a clone.

If you modify the internals of an object or array – by changing a property, or pushing a new item, or even modifying an item inside an array – then the object or array is referentially equal to its old self, and a PureComponent will not notice that it has changed, and will not re-render. Weird rendering bugs will ensue.

Remember our first example with Bob and the giveAwesomePowers function? Remember how the object returned by the function was exactly the same, triple-equal, ===, to the person that was passed in? That’s because both variables referred to the same object. Only the internals had been changed.

How Referential Equality Works in JavaScript

What does “referentially equal” mean? Ok, this’ll be a quick tangent, but it’s important to understand.

JavaScript objects and arrays are stored in memory. (You should be nodding right now)

Let’s say a place in memory is like a box. The variable name “points to” the box, and the box holds the actual value.

A variable points to a memory location

In JavaScript, these boxes (memory addresses) are unnamed and unknowable. You can’t figure out the memory address a variable points to. (In some other languages, like C, you can actually inspect the memory address of a variable and see where it lives)

If you reassign a variable, it will point to a new memory location.

reassigning a variable points it to a new memory location

If you mutate the internals of the variable, it still points to the same address.

Mutating a variable doesn't change its address

Much like ripping out the insides of a house and putting in new walls, kitchen, living room swimming pool, and so on – the address of that house remains the same. You don’t have to remind your relatives where to send the birthday money, because you still live at the same place.

Here is the key: when you compare two objects or arrays with the === operator, JavaScript is actually comparing the addresses they point to – a.k.a. their references. JS does not even peek into the object. It only compares the references. That’s what “referential equality” means.

So if you take an object, and modify it, it will modify the contents of the object, but it will not change its reference.

Another thing is, when you assign one object to another (or pass it in as a function argument, which is effectively doing the same thing), that other object is merely another pointer to the same memory location as the first object. It’s like a voodoo doll. Anything you do to the second object will directly affect the value of the first object too.

Here’s some code to make this a bit more concrete:

// This creates a variable, `crayon`, that points to a box (unnamed),
// which holds the object `{ color: 'red' }`
let crayon = { color: 'red' };

// Changing a property of `crayon` does NOT change the box it points to
crayon.color = 'blue';

// Assigning an object or array to another variable merely points
// that new variable at the old variable's box in memory
let crayon2 = crayon;
console.log(crayon2 === crayon); // true. both point to the same box.

// Niw, any further changes to `crayon2` will also affect `crayon1`
crayon2.color = 'green';
console.log(crayon.color); // changed to green!
console.log(crayon2.color); // also green!

// ...because these two variables refer to the same object in memory
console.log(crayon2 === crayon);

Why Not Check Equality Deeply?

It might seem more “correct” to check the internals of two objects against each other before declaring them equal. While that’s true, it’s also slower.

How much slower? Well, that depends on the objects being compared. One with 10,000 child and grandchild properties is gonna be slower than one with 2 properties. It’s unpredictable.

A reference equality check is what computer scientists would call “constant time.” Constant time, a.k.a. O(1), means that the operation will always take the same amount of time, regardless of how big the inputs are.

A deep equality check, on the other hand, is more likely to be linear time, a.k.a. O(N), which means the time it takes is proportional to how many keys are in the objects. Linear time is, generally speaking, slower than constant time.

Think of it this way: Pretend that every time JS compares two values like a === b it takes one full second to run. Now, do you want to do that once, to check the reference? Or do you want to descend into the depths of BOTH objects and compare each and every property? Sounds pretty slow, yeah?

In reality, an equality check is much much much faster than a whole second, but still, the principle of “do the least work possible” applies here. All else being equal, use the most performant option. It’ll save you time down the road trying to figure out why your app is slow. If you’re careful (and maybe a bit lucky), it’ll never get slow in the first place :)

Does const Prevent Changes?

The short answer is: no. Neither let nor const nor var will prevent you from changing the internals of an object. All three ways of declaring a variable allow you to mutate its internals.

“But it’s called const! Isn’t that supposed to be constant?”

Well, sorta. const will only prevent you from reassigning the reference. It doesn’t stop you from changing the object. Here’s an example:

const order = { type: "coffee" }

// const will allow changing the order type...
order.type = "tea"; // this is fine

// const will prevent reassigning `order`
order = { type: "tea" } // this is an Error

Keep that in mind the next time you see a const.

I like to use const as a reminder to myself that an object or array shouldn’t be mutated (which is most of the time). If I’m writing code where I know for certain I’ll be mutating an array or object, I’ll declare it with let. It’s just a convention, though. (and, like most conventions, if you break it every now and then, that’s about as good as having no convention at all).

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, just like we did up above with Bob. (And if you’re unsure, read about what a reducer is and where that name comes from)

Writing code to do immutable state updates can be tricky. Below, you will find a few common patterns.

Try them out on your own, whether in the browser developer console or in a real app. Pay particular attention to the nested object updates, and practice those. I find those to be the trickiest.

All of this actually applies to React state too, so the things you learn in this guide will apply whether you use Redux or not.

At the end, we’ll look at how to make it easier with a library called Immer – but don’t just skip to the end! If you’re going to be working on existing code bases, it’ll be very useful to understand how to do this stuff “long hand.”

The ... Spread Operator

These examples make heavy use of the spread operator for arrays and objects. Here’s how it works.

When this ... notation is placed before an object or array, it unwraps the children within, and inserts them right there.

// For arrays:
let nums = [1, 2, 3];
let newNums = [...nums]; // => [1, 2, 3]
nums === newNums // => false! it's a new array

// For objects:
let person = {
  name: "Liz",
  age: 32
}
let newPerson = {...person};
person === newPerson // => false! it's a new object

// Internal properties are left alone:
let company = {
  name: "Foo Corp",
  people: [
    {name: "Joe"},
    {name: "Alice"}
  ]
}
let newCompany = {...company};
newCompany === company // => false! not the same object
newCompany.people === company.people // => true!

When used as shown above, 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:

let liz = {
  name: "Liz",
  age: 32,
  location: {
    city: "Portland",
    state: "Oregon"
  },
  pets: [
    {type: "cat", name: "Redux"}
  ]
}

// Make Liz one year older, while leaving everything
// else the same:
let olderLiz = {
  ...liz,
  age: 33
}

The spread operator for objects is a stage 3 draft, which means it’s not officially part of JS yet. You’ll need to use a transpiler like Babel to use it in your code. If you use Create React App, you can already use it.

Recipes for Updating State

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. Of course in your own reducers you’ll probably have a switch statement with cases for each action, but I think that would just add noise here.

Updating State in React

To apply these examples to plain React state, you just need to tweak a couple things in these examples.

Since React will shallow merge the object you pass into this.setState(), you don’t need to spread the existing state like you would with Redux.

In a Redux reducer, you might write this:

return {
  ...state,
  (updates here)
}

With plain React state, you can write it like this, without the spread operator:

this.setState({
  updates here
})

Keep in mind, though, that since setState does a shallow merge, you’ll need to use the object (or array) spread operator when you’re updating deeply-nested items within state (anything deeper than the first level).

Redux: 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
  }
}

Redux: Update an Object in an Object

(This isn’t specific to Redux – the same method applies with plain React state. See here for how to adapt it.)

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
      }
    }
  }

This code can get hard to read when you’re updating deeply-nested items!

Redux: Updating an Object by Key

(This isn’t specific to Redux – the same method applies with plain React state. See here for how to adapt it.)

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
      }
    }
  }

Redux: Prepend an item to an array

(This isn’t specific to Redux – the same method applies with plain React state. See here for how to adapt it.)

The mutable way to do this would be to use Array’s .unshift function to add an item to the front. Array.prototype.unshift mutates the array, though, and that’s not what we want to do.

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
  ];

Redux: Add an item to an array

(This isn’t specific to Redux – the same method applies with plain React state. See here for how to adapt it.)

The mutable way to do this would be to use Array’s .push function to add an item to the end. That would mutate the array, though.

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;

Redux: Update an item in an array with map

(This isn’t specific to Redux – the same method applies with plain React state. See here for how to adapt it.)

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.

(If you have an array with N items and you want to end up with fewer items, use .filter. See Remove an item from an array, below).

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;
  });
}

Redux: Update an object in an array

(This isn’t specific to Redux – the same method applies with plain React state. See here for how to adapt it.)

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.

(If you have an array with N items and you want to end up with fewer items, use .filter. See Remove an item from an 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;
  });
}

Redux: Insert an item in the middle of an array

(This isn’t specific to Redux – the same method applies with plain React state. See here for how to adapt it.)

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.

Pro tip: Write unit tests for these things! It’s easy to make off-by-one errors.

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
  ];
  */
}

Redux: Update an item in an array by index

(This isn’t specific to Redux – the same method applies with plain React state. See here for how to adapt it.)

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;
  });
}

Redux: Remove an item from an array with filter

(This isn’t specific to Redux – the same method applies with plain React state. See here for how to adapt it.)

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” (or truthy). 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;
  });
}

Check out this Immutable Update Patterns section of the Redux docs for some other good tricks.

Easy State Updates with Immer

If you looked at some of the immutable state update code above and wanted to run away screaming, I don’t blame you.

Deeply-nested object updates are difficult to read, difficult to write, and difficult to get right. Unit tests are imperative, but even those don’t make the code much easier to read and write.

Thankfully, there’s a library that can help. Using Immer by Michael Weststrate, you can write the mutable code you know and love, with all the [].push and [].pop and = you can squeeze in there – and Immer will take that code and produce a perfect immutable update, like magic.

It’s awesome. Let me show you how it works:

First, you need to install Immer. (3.9K gzipped, according to this handy Import Cost plugin for VSCode. 2K according to Immer’s GitHub page. Either way – pretty small for how much awesomeness it adds)

yarn add immer

Then, you need to import the produce function from Immer. It only has this one export; this function is all it does. Which is great, nice and focused.

import produce from 'immer';

By the way, it’s called “produce” because it produces a new value, and the name is sort of the opposite of “reduce”. There’s an issue on Immer’s GitHub where they originally discussed the name.

From there, you can use the produce function to build yourself a nice little mutable playground, where all your mutations will be handled by the magic of JS Proxies. Here’s a before-and-after, starting with the plain JS version of a reducer that updates a value nested inside an object, followed by the Immer version:

/*
  State looks like:

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

function plainJsReducer(state, action) {
  // 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
      }
    }
  }
}

function immerifiedReducer(state, action) {
  const key = "ravenclaw";

  // produce takes the existing state, and a function
  // It'll call the function with a "draft" version of the state
  return produce(state, draft => {
    // Modify the draft however you want
    draft.houses[key].points += 3;

    // The modified draft will be
    // returned automatically.
    // No need to return anything.
  });
}

Using Immer with React State

Immer works well with plain React state, too – the “functional” form of setState.

You may already know that React’s setState has a “functional” form that accepts a function and passes it the current state. The function then returns the new state:

onIncrementClick = () => {
  // The normal form:
  this.setState({
    count: this.state.count + 1
  });

  // The functional form:
  this.setState(state => {
    return {
      count: state.count + 1
    }
  });
}

Immer’s produce function can be slotted in as the state update function. You’ll notice this way of calling produce only passes a single argument – the update function – instead of two arguments (state, draft => {}) as we did in the reducer example.

onIncrementClick = () => {
  // The Immer way:
  this.setState(produce(draft => {
    draft.count += 1
  });
}

This works because Immer’s produce function is set up to return a “curried” function when it’s called with only 1 argument. The function it returns, in this case, is ready to accept a state, and call your update function with the draft.

Gradually Adopting Immer

A nice feature of Immer is that because it’s so small and focused (just the one function that produces new states), it’s easy to add it to an existing codebase and try it out.

Immer is backwards compatible with existing Redux reducers, too. If you wrap your existing switch/case in Immer’s produce function, all of your reducer tests should still pass.

Earlier I showed that the update function you pass to produce can implicitly return undefined and that it’ll automatically pick up the changes to the draft state. What I didn’t mention is that the update function can alternatively return a brand new state, as long as it hasn’t made any changes to the draft.

This means your existing Redux reducers, which already return brand new states, can be wrapped with Immer’s produce function and they should keep working exactly the same. At that point, you’re free to replace hard-to-read immutable code at your leisure, piece by piece. Check out the official example of different ways to return data from producers.

Hassle-free blogging platform that developers and teams love.
  • Docs by Hashnode
    New
  • Blogs
  • AI Markdown Editor
  • GraphQL APIs
  • Open source Starter-kit

© Hashnode 2024 — LinearBytes Inc.

Privacy PolicyTermsCode of Conduct