In this tutorial, we’ll answer the following questions:
- What is immutability?
- Why and when should you keep your state immutable?
- Issues pertaining to mutations
Mutable and Immutable Data Types
A mutable object is an object whose state can be modified or changed over time. An immutable object, on the other hand, is an object whose state cannot be modified after it is created. Well, that’s how the textbook defines mutable and immutable objects.
Numbers, for instance, are immutable because you can’t change its value. For example, you can’t literarily change the value of 7 to 8. That doesn’t make sense. Instead, you can change the value stored in the variable x from 7 to 8.
Strings are also immutable too, and this is obvious when you try to change a particular character in a string.
But that doesn’t stop you from creating new strings.
Other string manipulation methods like
This is because strings and numbers are primitive value types that are assigned by value and not by reference. Here’s a good discussion on copy by value vs. copy by reference for starters.
You might also like: Do You Really Need Immutable Data?
When I was working on my product, Storylens — A free blogging platform, I’ve been using a combination of React and Redux. Both React and Redux give high consideration to shallow-equality checking. This ensures that the DOM rerenders only when an underlying object’s reference has changed. So when you update the state or the store in React/Redux, you’ll have to avoid mutating objects and arrays.
Similarly, most of the modern frontend stacks today rely heavily on Immutable data structures and if you’re going to use them, it might be a better idea to get used to these concepts. Let’s have a look at arrays and objects.
Arrays and Objects Are Mutable
As you can see, x is a mutable object, and any change in the property of x gets reflected in the value of y. Why? They all share the same reference. So, when you update the value of a property of x, you’re modifying the value for all the references to that object.
Let’s have a look at Arrays:
It’s certain that the value of y is going to contain both ‘foo’ and ‘bar’. But what about x?
Both x and y are references to the same item, and the method push mutates the original array. A lot of the array methods that you use every day are Mutator methods. Here’s the list of Array methods that operate on the original array.
Methods like map and filter are immutable because they create a new array without mutating the original array. You can verify this by running an example like this:
To avoid mutation, the option available in ES5 was freezing the object using Object.freeze. It wasn’t a great option either. With ES6, you can prevent mutating objects using
Object.assign() copies the values (of all enumerable own properties) from one or more source objects to a target object. It has a signature of Object.assign(target, …sources).
Let’s consider an example of objects:
The parameter to
Object.assign() method is the target object, and you can pass one more than one objects as sources. The method merges all the sources from right to left in that order and returns the target object. You can use the method for merging objects and cloning them shallowly.
Given its rather verbose syntax, Object.assign can make your code difficult to read. There’s an alternative syntax called object spread operator that uses three dots to shallow clone an object. The spread operator lets you use to copy enumerable properties from multiple sources using a succinct way. Here is how you clone objects using spread operator.
Deleting Object Properties
There are multiple ways that you remove a property without mutating the object. My personal favorite is using the object destructuring syntax.
Here is an example:
If you already know the name of the property to remove, you can try this:’
If the name of the property to remove is dynamic, you can do this:
As mentioned earlier, most of the array methods are mutable. But you can use the spread operator syntax on arrays to add and remove items from an array. Let’s have a look at an example:
Nice, that was easy! We kept the old array intact and created a new one. What about delete?
splice() method removes the item from the original array. So you could instead use
filter() which returns a new array.
Immutability in Array of objects and Nested Objects
Array functions like .map are immutable when you’re mapping an array of values. However, they’re not immutable when you’re working with an array of objects. Let’s expand on the fruits example that we created earlier.
Why is the original array’s data mutated as well? The map method clones the original array as expected, but since we are operating on an array of objects, each item in the array is a reference to the object in memory. Any modification that you make to an object in the cloned array will modify the original object via reference.
Why Is Immutability Important?
If you’ve been actively using frontend libraries like React, Vue, or a state management library like Redux, you might have come across warnings that say you shouldn’t mutate the state. Keeping the state immutable can help you in terms of performance, predictivity and better mutation tracking.
For any medium-sized application, there will be state — a lot of it — and asynchronous actions updating that state. The state of the app will be significantly different from the initial state after the end user starts using it. Mutation hides changes that result in side effects which in turn makes it hard to debug and find bugs. By keeping your structures immutable, you’ll be able to predict what’s in the state at any given time and you can rest assured that there won’t be any nasty side effects.
WIth immutable entities, you can see the changes that happen to these objects as a chain of events. This is because the variables have new references which are easier to track compared to existing variables. This can particularly help you with debugging your code and building better concurrent applications. There are event debuggers that help you replay DOM events with video playbacks that works entirely based on tracking mutation.