Immutable, a buzzword that I hear a lot but never quite understood the importance of it until recently. Personally for me, its difficult for me to see the importance of anything until I understand the practical applications for it. Immutability was a topic discussed towards the end of the last ReactNYC Meetup I attended which puts this term into perspective.
Nir Kaufman’s presentation on immutability primarily focused on optimization. Although I wasn’t able to fully understand each example while there, I did my own research afterwards. Slowly, but surely, I kept going down the rabbit hole on this concept, attempting to under the nuances of each example Nir provided, such as structural sharing, copy-on-write, and Immer.js. Although I am not an expert on this topic by any means, I did learn a lot of new optimization techniques that may help any future coder.
Before we dive into the techniques, let’s first discuss what it means when an object is immutable. According to Wikipedia’s definition:
We can go even broader and look at Merriam-Webster’s definition of immutable:
: not capable of or susceptible to change
For the sake of analogy, one can think of a
const variable assigned to a primitive data type as being immutable. By the definition of a
const variable, it cannot be re-assigned a new value after initialization. Because a value cannot be re-assigned, the
const variable can be seen as immutable.
Note: All examples and techniques shown below correlates more towards Merriam-Webster’s definition of immutable than Wikipedia’s definition as I am primarily focusing on the “cannot be modified” aspect.
From my research, and Wikipedia’s definition, it seems that immutability normally refers to objects.
Line 1 instantiated the object, line 3 changed an existing key-value pair, and line 5 assigned a new key-value pair.
However, what if I wanted to make the object immutable after changing and adding those key-value pairs? You can with
Object.freeze() ! According to its documentation:
Object.freeze()method freezes an object. A frozen object can no longer be changed; freezing an object prevents new properties from being added to it, existing properties from being removed, prevents changing the enumerability, configurability, or writability of existing properties, and prevents the values of existing properties from being changed. In addition, freezing an object also prevents its prototype from being changed.
freeze()returns the same object that was passed in.
In other words, no one can change the object in any way, shape, or form… for the most part. Let’s test this function out from our previous example.
On line 8, a new variable called
immutableObject was instantiated with the frozen
mutableObject variable. Lines 9 and 10 changed and added key-value pairs to
immutableObject but line 12 shows that nothing changed within the object.
Huzzah! Looks like we made out object immutable at this stage. Another fun fact with
Object.freeze() is if you store the frozen object in a new variable, it actually points to the same place in memory as the object you wanted to freeze. Line 13 shows that.
Getting back to the for the most part,
Object.freeze() does not work for nested objects. This implies that
Object.freeze() does a shallow freeze on the object that you pass through. The gist below demonstrates this:
As you can see from the gist above, in line 5, I changed the value in the nested object and it showed in line 6. If you want to freeze a nested object, you would need to freeze each layer of the object whose value is an object type.
The beauty, and also confusing part, of Immer.js is that it uses structural sharing. Structural sharing is analogous to persistent data structure, in which Wikipedia defines it as:
In computing, a persistent data structure is a data structure that always preserves the previous version of itself when it is modified. Such data structures are effectively immutable, as their operations do not (visibly) update the structure in-place, but instead always yield a new updated structure.
In light of learning another new topic, I had to scan far and wide into the abyss of Google to further my knowledge. I came across two blogs that goes into detail on structural sharing and Immer.js/
Introducing Immer: Immutability the easy way
Immutable, structurally shared data structures are a great paradigm for storing state. Especially when combined with an…
Immutable.js, persistent data structures and structural sharing
Essentially, structural sharing keeps the original structure as before and the only portion of it that changes is creating a new portion in the original structure that points to the new value. By mostly keeping the original structure, it is effectively making the object/array immutable.
Let’s take a quick step back and look at reducers in your Redux store. Conventionally, if there were any changes to the state in your reducer, you would use the spread operator to make a copy of the original state, apply any necessary changes, and then update the state with the new changes. By using the spread operator, you are copying and creating new data, line by line, within your object/array although the data is exactly the same as before. If this is scaled for 100,000 items, you would be duplicating 100,000 items (with the exception of where it needs to be updated), and therefore eating a ton of memory and slowing down your application.
With Immer.js, and its
produce() function, structural sharing avoids this from happening. For argument’s sake, in an object, in order to access a value based on its key, the computer follows a “route”. Each possible “route” makes up the original structure of the object.
Note: The term “route” is not an actual term used in the this topic but I created it for ease of explanation.
Now, lets say that you need to update an item within your state. With Immer.js, it can determine the specific “route” where the item needs to updated and keep the “routes” of all the non-updated items through structural sharing. This means that a new “route” is created to reflect the new value whereas all the old “routes” are the re-used/the same because there are no changes to its current value. If you were to use the spread operator in this analogy, you would be re-creating all the same “routes” although they all point to the same previous value.
If you would like to read about the data structure and understand the technical, proper explanation, please read about tries.
Although I didn’t get into copy-on-write, please feel free to explore the topic yourself. The key takeaway from all of this research is that code can always be optimized in some way. Although this blog was referring to optimizing with respect to speed, you can also optimize with respect to memory, like refactoring your code. There are small ways to optimize as well such as memoization (e.g.,
React.memo() ). Every little bit of optimization will go a long way.