Autoplay

    Simplify Creating Immutable Data Trees With Immer

    Michel WeststrateMichel Weststrate

    Immer is a tiny library that makes it possible to work with immutable data in JavaScript in a much more straight-forward way by operating on a temporarily draft state and using all the normal JavaScript API's and data structures. The first part of the lesson explains how to use Immer, and the second part of the lesson shows you how it can be used to simplify, for example, Redux reducers.

    Immer has a few unique features:

    • Supports arbitrary, deep modifications in immutable data trees
    • Strongly typed
    • Uses JavaScript native data structures, no new API to learn
    • Automatically freezes any data that is produced
    reduxRedux
    javascriptJavaScript
    Code

    Code

    Become a Member to view code

    You must be a Member to view code

    Access all courses and lessons, track your progress, gain confidence and expertise.

    Become a Member
    and unlock code for this lesson
    Transcript

    Transcript

    Instructor: 00:00 Working with immutable data structures and Java Script has always been a bit tricky, and that is mostly because the language doesn't offer immutable data structures or operations related to that out of the box.

    00:12 However, you will notice that with IMMR these days are over. That is because IMMR has a very unique way of working with immutable data, and it has a few very unique features. I just created a small data structure, a list of to-dos, and from now on we will consider it to be immutable.

    00:33 IMMR exposes one important function. It doesn't matter how you name it, but we call it produce in this tutorial. Let's start using it. What we want to do is to produce our next set, our next set of to-dos, without mutating the current set.

    00:55 Produce takes two arguments, the state it starts with, and a function which has an argument in itself called the draft. On the draft we're going to work. All the mutations we make to the draft will be expressed ultimately in the next to-dos returned from this producer.

    01:17 Now, the simplest producer we can write is a producer that doesn't do anything, like we have here. What you now see is that it still produces a next state, but it is a next state which is completely equivalent to the current one, so it has two to-do items.

    01:35 Both are not done, in effect, if we check the equality of the whole state tree, you will see that it's literally the same tree as we had before. Our next to-do list is literally the same as our original to-do list. Now, here is the interesting thing. The draft is actually a proxy, a layer, of the original state tree.

    01:57 What we can do is that we modify the draft, and you will notice that we have auto-completion here from tab script, because well, we're just working with common data structures, so tab scripts knows about these types.

    02:11 I would just put a new item onto the draft. We will now see that the next to-do sets has a length of three, and that the newly produced state is no longer the original one.

    02:24 However, it still structurally shares the items inaudible with the previous state, so both to-do items which already existed are still the same. Now let's do another mutation. Let's modify the done status of the first to-do.

    02:40 What you will notice first is that we can make direct and deep mutations to our state tree. The second thing you will notice is that creating these mutations is just using the normal Java Script operations.

    02:56 There is no separate API you have to learn here to work with immutable data. There are no string based bar selectors or query like syntaxes you need to learn to be able to work with this data. It's all just plain Java Script structures.

    03:13 We toggle the to-do item to true. What we now see is that from the original state the first to-do item is still not done, but in the first state the to-do item is done. In fact, you see that also the first to-do in the new state is no longer structurally shared with the previous state, but the second one still is.

    03:33 You can see that whenever we make a mutation to the draft it doesn't affect the original state, but it will affect the new state. In fact, IMMR does something very fancy for us out of the box. It will also automatically freeze for us the new state.

    03:51 If we now try to modify that first to-do again outside the produce function, it will show an exception that the object is read only. In other words that it is truly immutable. It is time to see how convenient this all is in practice. Let's apply IMMR for example to a Redux reducer.

    04:15 Here is the official shopping cart example from the Redux inaudible . Here is the most complex reducer of that product, the product reducer. On the right hand you see the test suite running, and we keep it running to show you that semantically we aren't changing anything.

    04:36 Here is the first reducer, the products. It supports one action, add to cart, and it means that it increases the inventory of that product by one. Originally, it took the original state of a product and then spread it out, create new object from it, but with a lower inventory.

    04:56 With IMMR we can just say the next state is produced from the current state, and what we apply to the draft is that whenever the action type is add to cart, we reduce the inventory by one.

    05:11 Now we can remove the default case, because as you saw earlier, one produce doesn't change anything, it just returns to current state. You can see how this simplifies this reducer greatly and makes it much easier to read.

    05:27 Now, let's take a look at the second reducer. Now again we wrap this reducer in produce, and here is this action block and you might be wondering when you read it, what is it actually doing? It takes the current state, then it reduces of the product, and restores all products in an object, and in the end that object is merged back to the original state.

    05:52 Now, that might be a bit confusing to read, so let's write this much more straightforward. Instead we just loop over all the products in our action. These are the incoming products. To the draft state we just add this product on the product ID. That's what the reducer basically said.

    06:07 For the second case, what is done there is that the product reducer is called and its result is stored in state. Let's keep it that way but just simplify it. By default, we invoke the products reducer, store it in a draft state in the product ID, and you will note that the entire test suite passes. This application behaves completely the same, except that we simplified it.

    06:36 There is one last thing I want to show you. The produce function of IMMR so far always accepted two arguments, the current state and a recipe function. However, it does support carrying out of the box.

    06:50 That means, if you pass it only one function, the recipe, then you can use that to create a new function that later on accepts the state and applies the recipe to the current state, and produces the next state. We can use that to write our reducers in an even more simple way.

    07:06 Our product reducer is now created by calling produce with just one argument, the recipe function. Which takes the draft and also it will receive any other arguments that will pass through the resulting function. That means that we now still have the same reducer, as you can see by the results of test suite.

    07:25 This is now what our div look like. We lost all the complex manual object structuring, and instead we made our reducers much more concise and still producing an immutable structurally shared state.

    Discuss

    Discuss