What is Immer ? how to understand shallow and deep cloning in Javascript!

Divyanshu Negiβ€’

β€’

πŸš€πŸš€πŸš€πŸš€ 6 min read

This blog is going to be fun. I moved from JS to Java early in my development career.

I never got time to understand the JS ecosystem deeply. Later, I moved from Java to JS, and most things did not make sense to me in terms of the object-oriented and class-based to a more functional approach to building things.

In this process, I stumbled upon a code with the heavy use of immer !

That piqued my interest in this library and why we would need such a thing in JS. Ok, wait, hold on, take a deep breath. Lets learn something very, very interesting about JS

How cloning works in JS

Let's assume we have this Object


const user = {
	username: "divyanshu",
	dob: "13-10-1990",
	phone: "+919998887767"
}

const updatedUser = user

updatedUser.username = "divyanshu2"

console.log(user, updatedUser)

When we print this

{ username: 'divyanshu2', dob: '13-10-1990', phone: '+919998887767' } 
{ username: 'divyanshu2', dob: '13-10-1990', phone: '+919998887767' }
[Finished in 141ms]

As you can see, both Object printed, but the value is the same as modified, which means when we assigned user to updatedUser it did not copy the values but copied the reference to that Object to updatedUser.

In simple terms, if in memory user has reference address as 0x001 where this Object is stored, and we later did updatedUser = user , this means we assigned 0x001 to updatedUser, and not the whole Object was copied to updatedUser , now any change made to updatedUser would reflect in user object as well because both of them share the same memory, and logically we are talking about the same Object with two different names.

Ok, Now the problem with this is, in our application, how can we copy the data from 1 object to another, so this kind of change on the original Object can be stopped?

This is also known as immutability.

Immutability

In object-oriented and functional programming, an immutable object is an object whose state cannot be modified after it is created. This is in contrast to a mutable object, which can be modified after it is created

It is a good practice in development. You want all your Object to be immutable unless there is a strong reason not to do it.

This keeps side-effects to a limit. One change somewhere in the application does not affect another part of your application.

Back to cloning in JS

Ok, so how can we copy an object with confidence?


const user = {
	username: "divyanshu",
	dob: "13-10-1990",
	phone: "+919998887767"
}

const updatedUser = {...user} // Added a spread operator

updatedUser.username = "divyanshu2"

console.log(user, updatedUser)

With ES6, we have a new spread operator denoted with .... This does not pass the reference but copies the content from the Object to the assigned Object.

Here is the output with this change

{ username: 'divyanshu', dob: '13-10-1990', phone: '+919998887767' } 
{ username: 'divyanshu2', dob: '13-10-1990', phone: '+919998887767' }
[Finished in 56ms]

As you can see now, the value of username is different on both Objects, change in updatedUser does not affect the old user object. Perfect! Problem solved.

Let's go home and build the next billion-dollar application now.

wait...

This is JS. It cannot be so simple in this world.

Ok, so let us try another little complex Object. We have now just added a new object as profile in our user object, which is an object containing image and a verified boolean.

And in the copied updatedUser, we change the verified to true.


const user = {
	username: "divyanshu",
	dob: "13-10-1990",
	phone: "+919998887767",
	profile: {
		image: "profile.png",
		verified: false
	}
}

const updatedUser = {...user} // Added a spread operator

updatedUser.username = "divyanshu2"
updatedUser.profile.verified = true

console.log(user, updatedUser)

Print this now.

{
  username: 'divyanshu',
  dob: '13-10-1990',
  phone: '+919998887767',
  profile: { image: 'profile.png', verified: true }
} {
  username: 'divyanshu2',
  dob: '13-10-1990',
  phone: '+919998887767',
  profile: { image: 'profile.png', verified: true }
}
[Finished in 51ms]

Hmm, now you see the spread worked on username, profile again changed for old Object with change in new updatedUser Object.

Why is that?

Because in JS, every Object, when created, is stored in a memory (Obviously), but when copied, it only passes the reference.

So we only spread the user object, and never told anything about profile Object. Hence when it was copied, it again passed its reference (duh!).

how to solve this, well quick and not so easy solution,


const user = {
	username: "divyanshu",
	dob: "13-10-1990",
	phone: "+919998887767",
	profile: {
		image: "profile.png",
		verified: false
	}
}

const updatedUser = {...user, profile : {...user.profile}} // Added a spread operator on profile too

updatedUser.username = "divyanshu2"
updatedUser.profile.verified = true

console.log(user, updatedUser)

This would work now, as we added this spread on profile as well.

const updatedUser = {...user, profile : {...user.profile}}

The problem here though, is, for a straightforward object, we would know the keys, which are Object, but what if the Object becomes more complex and the data might be coming from the backend, which we don't even know about unless we parse it.

This can become a huge problem to solve.

Shallow Cloning and Deep Cloning

The way we copied with spread operator where nested Object not copied but passed as reference is called shallow cloning, as we never cloned the complete Object.

Deep cloning is a method when we are 100% sure that the newly copied Object does not have any reference to any other object in our code.

How to do it in super easy way.

const user = {
	username: "divyanshu",
	dob: "13-10-1990",
	phone: "+919998887767",
	profile: {
		image: "profile.png",
		verified: false
	}
}

const updatedUser = JSON.parse(JSON.stringify(user))

updatedUser.username = "divyanshu2"
updatedUser.profile.verified = true

console.log(user, updatedUser);

Yup, the most Stupidly easy way.

you stringify the Object, which means convert it to a string, and later parse the string to convert it to a JSON object.

This will guarantee that whatever is copied is copied 100% deep clone.

I would never prefer this way of cloning the data.

Problem 1

Some of the data can be lost with this approach, it works pretty good with the above example, but if our keys contain values like undefined, function, all of those would be converted to {}.

Problem 2

Doing this stringify and parse is the most inefficient way. It takes a lot of time to convert Object to string and later to an object. Compared to other methods of deep cloning, this way is the slowest.

We have other manual ways and libraries which can help with deep cloning.

  1. Lodash (https://lodash.com/docs/#cloneDeep)
  2. Native deep cloning (experimental)
  3. Iterating through each object property and copying it into a new empty object

But apart from those, to work in an immutable way, where you never make changes to the original Object, one of my favourites is immer

What is Immer?

Immer isΒ a small library created to help developers with immutable states based on a copy-on-write mechanism, a technique used to implement a copy operation on modifiable resources

This is the definition of this library. To me, as a noob in JS back in 2016, this made no sense. The main reason for not being able to understand how everything in JS is actually an Object, most of the time we don't create Object in JS but rather work with functions (which themselves are first-class Objects)

As per our previous example, how can we modify the values without changing the original Object by using immer?


const user = {
	username: "divyanshu",
	dob: "13-10-1990",
	phone: "+919998887767",
	profile: {
		image: "profile.png",
		verified: false
	}
}

const updatedUser = immer.produce(user, draft => {
	draft.profile.verified = true
})

console.log(user, updatedUser);

with immer installed in your dependencies, you can simply use immer.produce produce takes 2 arguments, 1 is the Object which we want to copy and modify, and another is a function which will have draft param.

Here draft is the deeply cloned copy of the first passed Object, and in the function, you can make the changes to the draft.

All the changes made to the draft will be immutable, and the original Object remains unaffected.

This is the simplest and fastest way to achieve immutability in the code.

I hope this will be helpful in understanding something new in JS.

πŸ™ Thanks for reading.

X

Did this post help you ?

I'd appreciate your feedback so I can make my blog posts more helpful. Did this post help you learn something or fix an issue you were having?

Yes

No

X

If you'd like to support this blog by buying me a coffee I'd really appreciate it!

X

Subscribe to my newsletter

Join 107+ other developers and get free, weekly updates and code insights directly to your inbox.

  • No Spam
  • Unsubscribe whenever
  • Email Address

    Powered by Buttondown

    Picture of Divyanshu Negi

    Divyanshu Negi is a VP of Engineering at Zaapi Pte.

    X