The Importance of Immutability in Programming: Why It Matters?

ArturArtur
5 min read

A lot of folks starting out with JavaScript and programming, in general, often encounter the idea of immutability and its importance. But why is it so crucial? And why does it matter?

What exactly is immutability and an immutable value?

Immutable means something that cannot be changed. So, in simple terms, an immutable value is a value that cannot be changed after it has been created. It's not that difficult to understand. In JavaScript, primitive data types (boolean (true or false), number, string, null, undefined, bigint, and symbol) are all immutable.

Let's compare some values:

5 === 5; //true 
"Hello Arthur" === "Hello Arthur"; //true 
true === true; //true 
false === false; //true (because they're the same)

Let's check arrays and objects:

[] === []; //false
{} === {}; //false 
[1] === [1]; //false 
{key: "Optimus"} === {key: "Optimus"}; //false

Hmm... that's strange. Even if we use ==, it will still result in false. Why? Because both Arrays and Objects are classified as Objects in JavaScript. An Array is an instance of Object. Whenever we use [] or {}, we are making a new instance of an Array and Object, respectively. You can rewrite it like this:

new Array === new Array; //false
new Object === new Object; //false
const arr1 = new Array();
arr1.push(5);
const arr2 = new Array();
arr2.push(5);
arr1 === arr2 //false;

When we use the new keyword, we get a new instance, which means new Array() is not the same as new Array() because they are different instances.

There is one more thing we need to understand, which is Object assignment. This might seem a bit strange (and it is, in a way), but let's look at this example:

const firstArray = [];
const secondArray = firstArray; //secondArray  poins to the firstArray 
console.log("First array", firstArray); //Output: First array[]
console.log("Second array", secondArray); //Output: Second array[]

firstArray.push(300);
console.log("First array", firstArray); //Output: First array[300]
console.log("Second array", secondArray); //Output: Second array[300]

Wow, what happened here? Well, the issue is that we didn't actually copy the first array; we just created a reference to it. This means that secondArray points to the same memory location as firstArray.

Let's take a look at this example:

const numbers = [0, 1, 2, 3];
const reverseNumbers = numbers.reverse();
console.log(reverseNumbers) //Output: [3, 2, 1, 0]
console.log(numbers) //Output: [3, 2, 1, 0]

The array.reverse() method mutates (changes) the original array. Other array methods that mutates the array:
.pop(), .push(), .shift(), .unshift(), .reverse(), .sort(), .splice(), .fill(), copyWithin()

How to update value immutably in JavaScript?

To make objects and arrays immutable in JavaScript, you can use specific techniques to generate new values without altering the original content. Essentially, you need to make a new copy to retain the original variable's value. This process is known as immutable array operations. Let's consider the following example:

const numbers = [0, 1, 2, 3];
const reverseNumbers = numbers.toReversed(); //Notice how we used .toReversed() method
console.log(reverseNumbers) //Output: [3, 2, 1, 0]
console.log(numbers) //Output: [0, 1, 2, 3]

We used the .toReversed() method to create the reverseNumber array without altering the numbers array. This method copies the numbers array and performs the operation on this duplicate array. This is the core idea behind immutability.

You can use the spread operator to create an immutable copy of an array or object, known as a shallow copy:

// Original array
const originalArray = [1, 2, 3, 4];

// Creating a shallow copy using spread syntax
const copiedArray = [...originalArray];

// Modifying the new array without affecting the original
copiedArray.push(5);

console.log(originalArray); // Outputs: [1, 2, 3, 4]
console.log(copiedArray); // Outputs: [1, 2, 3, 4, 5]
// Original object
const originalObject = { a: 1, b: 2, c: 3 };

// Creating a shallow copy using spread syntax
const copiedObject = { ...originalObject };

// Modifying the copied object without affecting the original
copiedObject.d = 4;

console.log(originalObject); // Outputs: { a: 1, b: 2, c: 3 }
console.log(copiedObject); // Outputs: { a: 1, b: 2, c: 3, d: 4 }

Also, you can update Arrays immutably using these array methods:
.map(), .filter(), slice(), from(), .concat(), reduce()

Why is this so important?

  1. Predictability and Debugging: Immutable data remains consistent throughout the application, which makes reasoning about the state easier. This way we prevent side effects that can lead to bugs. This is particularly important in large codebases or when working with complex data structures. In other words, your code becomes more predictable, enabling you to identify changes efficiently.

  2. Concurrency: Since immutable data cannot change after it is created, it is inherently safe to share across threads without risk of data corruption. A thread is the smallest series of related instructions involved in a process, which can involve many threads.

  3. Change Detection and Optimization: Many frameworks, like React, rely on immutability to efficiently detect changes. When data is immutable, you can safely compare old and new states with a simple reference comparison, which is much faster than deep comparisons.

  4. Undo and History Tracking: With immutable data structures, tracking the history of changes becomes easier. Previous states can be retained and re-used without worrying about unintended modifications, which is valuable for features like undo/redo. Redux (for JavaScript/React applications) employ immutability principles to manage application states efficiently. By keeping the state immutable and maintaining a history of changes, developers can easily implement features like undo/redo or view a previous state for debugging.

Conclusion

Immutability is a fundamental concept that brings predictability, safety, and clarity to software development. By treating data as immutable, developers can more confidently manage complex state transitions, enable efficient concurrent processing, and minimize error-prone operations. Although it may initially seem restrictive, immutability ultimately fosters a more resilient and maintainable codebase, reducing debugging headaches and unlocking powerful features like time-travel debugging.

Whether you are working on large-scale enterprise software or a small hobby project, incorporating immutability practices can vastly improve your application's reliability and ease of use. By embracing immutability, you can focus more on building creative solutions while knowing your data is secure, consistent, and easy to track.

If you liked this post, please give it a like and share it with others who might find it helpful!

0
Subscribe to my newsletter

Read articles from Artur directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Artur
Artur