Immutability, Data Types in JS, and updating non-primitives using Immer

Immutability, Data Types in JS, and updating non-primitives using Immer

ยท

4 min read

Hello everyone ๐Ÿ‘‹๐Ÿ‘‹, in this blog we're going to explore the immutability, data types in JS, why immutability is important and how we can achieve immutability very easily using immer.

Starting with our very first question ๐Ÿค”๐Ÿค”

What is immutability?

Immutability means the variable isn't changed over the run-time of a program.

Ahh not clear ๐Ÿค”? But, it is what it is. The variable whose value is not changed over run time is known to be immutable.

Immutability is one of the fundamentals in the functional programming paradigm that ensures the variable needs to be immutable.

How immutability is used and how it's important ๐Ÿค”๐Ÿค”?

Taking an example of React, every state is immutable as it is defined with the keyword const which ensures it can't be updated directly.

But what's the problem with updating the variable directly? In react, states are compared by the React and when it finds out the new reference/new variable is created then it triggers the re-render as React checks the state/props using the reference equality.

If some variable is updated, then a new reference is created that leads to predictable state updates using pure functions over the whole application as React and redux re-renders after reference equality check over the state and when the reference changes then it'll trigger the new state updates.

What is a pure function?

The pure function takes the input and returns the same output for the same input and has no side effects.

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

add is a pure function as it will always return 5 for a=3 and b=2.

What are the side effects?

Side effects involve the updates of the global variables, doing API requests, console logs and anything that is done outside the function.

const arr = [2,-1, 10, -10];
function min(arr){
    return arr.sort()[0];
}

min(arr);
console.log(arr);

arr.sort() performs the in-place sorting which updates the array in the sorted order and the array arr is mutated which is in the global scope. Therefore, it's an impure function with side effects leading to variable updates outside its scope.

Pure function version:

const arr = [2,-1,10,-10];
function min(arr){
     const temp = [...arr];
     return temp.sort()[0];
}

Data Types in Javascript

There are two types of data types:

  • Primitive data types: It includes strings, numbers, undefined, and null which are immutable in nature. They're only re-referenced to the new address location storing the updated values.
  • Non-primitive data types: It includes objects and arrays which are mutable in nature because the object reference remains the same while the values inside the object and array are updated.

Copying Non-Primitive data

const obj = {
  name: "Saurabh",
  online: true,
}

const obj2 = obj;

obj2.online = false;

obj2 is also referencing to the same obj and updates the object which leads to updates in both the variables.

In order to copy non-primitives, we need to use ... operator or anything which copies the element to the object with the new reference for every inner element.

Here's one way using ... spread operator.

const obj = {
  name: "Saurabh",
  online: true,
}

const obj2 = {...obj};

obj2.online = false;

Now, obj and obj2 has different references that make obj2 a deep copy of obj.

What about the nested non-primitive data like nested objects and arrays?

A simple solution for deep copying the nested objects or arrays using ... over every non-primitive recursively creates new references to every non-primitive nested data.

const obj = {
    nestOne: {
      nestTwo: {
        nestThree: {
          name: "Saurabh",
        },
      },
    },
  };

  const obj2 = {
    ...obj,
    nestOne: {
      ...obj.nextOne,
      nestTwo: {
        ...obj.nextOne.nextTwo,
        nestThree: {
          ...obj.nextOne.nextTwo.nextThree,
          name: "Chris",
        },
      },
    },
  };

But, doing deep copy using ... spread operator is cumbersome ๐Ÿ˜ฅ.

Ways to deep copy and update the keys in the non-primitive with some ease ๐Ÿ˜„๐Ÿ˜„

  • Lodash's cloneDeep() ``` const obj = { nestOne: {
    nestTwo: {
      nestThree: {
        name: "Saurabh",
      },
    },
    
    }, };

const obj2 = _.cloneDeep(obj); console.log(obj === obj2); // false

Lodash's `cloneDeep()` copies the non-primitive data deeply with new address locations/references. It copies each and every nested non-primitive deeply.


* **Immer's produce()**

import produce from "immer";

const obj2 = produce(obj, draft => { draft.nestOne.nestTwo.nestThree.name="Chris"; }) `` Immer'sproduce()` function takes the object as the first argument and in the second argument(a function) makes the updates in the draft(intermediary step while copying and updating object) and returns a newly updated object.

Which one is better to use Immer's produce() or Lodash's cloneDeep() ๐Ÿคท๐Ÿคท?

Talking in the context of the react and redux, immer's produce() is used as it memorizes the non-updated states and only changes the reference to the updated states which favours the principle of re-render of components only when the reference to the state/props is changed otherwise if the lodash's cloneDeep() is used then it copies everything which gives every state a new reference which fails the reference equality check and the every component will re-render on every state updates.

I hope you liked reading it and found it interesting and informative.๐Ÿš€๐Ÿš€

You can connect with me on @saurabh22suthar.

ย