Immutable Data

#ProgrammingThu Oct 17 2024

Immutable. It means 'unchangeable'. But why is immutability of data important? While I’ve heard the term “immutable objects” many times, I hadn’t really thought about why they’re significant. In this post, we’ll explore what immutable data is, why it matters, and take a look at the Immutable.js library.

Definition of Immutable

Let's start by looking at the definition of immutable from MDN.

An immutable object is one whose content cannot be changed. Objects can be immutable for several reasons:

  • To improve performance (as the object won’t be modified later)
  • To reduce memory usage (by referencing the object instead of duplicating it)
  • Thread safety (in multi-threaded programming, different threads can reference the same object without interfering with each other)

Immutability in JavaScript

While researching immutability, I came across a JavaScript Immutability course by Egoing, and here’s a summary of what I learned.

Immutability is about preventing the original data from being altered. When working with data, creation and reading are key since they involve the original state. It’s a good practice to “freeze” parts of your app that don’t need to change.

Many people assume that all data should be editable or deletable, but in some cases, especially where security is involved, this isn’t the case! (e.g., blockchain)

Variable Assignment

Let’s first explore how variables are assigned in JavaScript. How do variables point to values? There are two types of data in JavaScript: primitive data types and object data types. Primitive types include numbers, strings, booleans, null, undefined, and symbols. Object types include objects, arrays, and functions.

Take a look at the example below:

const p1 = 1
const p2 = 1

p1 === p2 //true

const o1 = {name: "kim"}
const o2 = {name: "kim"}

o1 === o2 //false

Here, p1 and p2 are both 1, which is a primitive type (number) and is therefore immutable. However, objects can change, so even though o1 and o2 contain the same values, they are stored separately.

Copying Objects

If you want to change a value without modifying the original data, you can create a copy and change the copy!

const o1 = {name: "kim"}
const o2 = o1
o2.name = "lee"

In this case, the property of o1 is changed, which isn’t what we want.

const o1 = {name: "kim"}
const o2 = Object.assign({}, o1)
o2.name = "lee"

Now, o2 is a copy of o1, so modifying the copy doesn’t affect the original. The value of o1 remains immutable.

We use the Object.assign() method to copy the properties of an object.

Nested Objects

When you copy an object with Object.assign, only the object’s properties are copied. If one of the properties is itself an object, it’s only the reference that gets copied, not the actual value. So, even though we copied o2, changes to the nested object will still affect o1.

const o1 = {name: "kim", score: [1, 2]}
const o2 = Object.assign({}, o1)
o2.score.push(3)

You might think that modifying o2 won’t affect o1, but check the results:

console.log(o1) // {name: "kim", score: [1, 2, 3]}
console.log(o2) // {name: "kim", score: [1, 2, 3]}

Both objects are affected because the score array’s reference was copied, not its value.

To modify only o2, you can use concat:

o2.score = o2.score.concat()
o2.score.push(3) // modifies the copy

push changes the original, but concat creates a copy. Using this approach, we preserve the immutability of the original o1!

The Array.prototype.concat() method is used to copy an array.

Creating Immutable Functions

function fn(person) {
    person = Object.assign({}, person);
    person.name = "lee";
    return person;
}
var o1 = {name: "kim"}
var o2 = fn(o1);

console.log(o1); // {name: "kim"}
console.log(o2); // {name: "lee"}

By returning a copy inside the function or passing a copied value to the function, we ensure immutability.

function fn(person) {
    person.name = "lee";
}
var o1 = {name: "kim"}
var o2 = Object.assign({}, o1)
fn(o2); // pass the copy

console.log(o1); // {name: "kim"}
console.log(o2); // {name: "lee"}

Object.freeze

Is there a way to prevent anyone from modifying an object entirely? Yes! You can freeze the object.

Use Object.freeze() to freeze an object and prevent modifications.

const o1 = {name: "kim", score: [1, 2]}
Object.freeze(o1);
o1.name = "lee";
console.log(o1); // {name: "kim", score: [1, 2]}

Functional Programming

In functional programming, components should always produce the same result. To achieve this, functions must maintain a pure state. For this, we need immutable input values (objects). Using immutability, we can create pure functions and use them as reliable components.

Immutable.js

Now, let’s dive into the Immutable.js library. Why was this library created?

Lee Byron, who works at Facebook, contributed to building both GraphQL and this library. You can check out his talk on Immutable Data and React at React.js Conf 2015.

Maintaining immutability can be tricky, and there are a lot of things to watch out for. This library helps reduce the barriers to using immutable data by enforcing immutability and mitigating the performance impact of copying data.

In short, Immutable.js is a library that provides immutable collections for JavaScript. Let’s look at a sample code he presented in the talk:

import { List } from "immutable";
var list = List.of(1, 2, 3); // List [1, 2, 3]
var list2 = list.push(4); // List [1, 2, 3, 4]
list // List [1, 2, 3]
list2 === list // false

Even though we used push, the original list remained unchanged because a copy was made and modified.

List.prototype.push = function (value) {
    var clone = deepCopy(this);
    clone[clone.length] = value;
    return clone;
}

By ensuring we always work with copies, the original data remains untouched.

API

Let’s take a look at some of the key APIs in Immutable.js:

  • List: Like a JavaScript arraytLike a JavaScript array
  • Map: A key-value based collection
  • Set: A collection with unique values
  • get(): Returns the value associated with a key
  • update(): Updates a value and returns the new collection
  • remove(): Removes a key and returns a new collection