Skip to content

Instantly share code, notes, and snippets.

@darleykrefta
Created March 20, 2021 12:05
Show Gist options
  • Save darleykrefta/ed5c4419b74fa91afa80b84961dc39fd to your computer and use it in GitHub Desktop.
Save darleykrefta/ed5c4419b74fa91afa80b84961dc39fd to your computer and use it in GitHub Desktop.

Immutability in JavaScript

When we use moderns libraries or frameworks the first important thing is to know how they work and manipulate data, in React, Angular, Svelte and in some cases Vue, we need to manipulate it in an immutable manner. An immutable code can be explained by something that never changes, a variable cannot be modified after it is created. Using JavaScript, we are going to see some types of data and mutable examples, understand about it and refactoring in an immutable way.


JavaScript types

To use immutability concept in JavaScript is very simple, but before we starting code it, we need to learn about data types in JavaScript.

Types into JavaScript get different ways to store the variable values. Variables are categorized in two types, value (primitives), and reference (non-primitives).


Primitive type

The primitive type (or value type) works manipulating their values basicaly doing a copy of our own values, in this way they are immutable by definition (the value never changes, but is replaced by a new one), in this category we have types such as number, string, boolean, symbol (es6), undefined and null. The immutability of primitive types can be exemplified by:

const name = 'Han'
let fullName = name

fullName = fullName + ' Solo'

// variable name was not affected
console.log(name)
// Han

// only variable fullName was updated
console.log(fullName)
// Han Solo

So, primitives are copied by their values.


Non-primitive type

Non-primitive types works a little different, the variable is assign to a reference in your declaration and this reference is assign to a value. Non-primitive category have types such as object, function and array. When we assign a variable with one of these types to another, we pass your reference to the new variable created, therefore when we change its value, the reference won't be changed, only your value, so we can have a problem with this behavior, because the new variable modify the older by reference, therefore the non-primitive types aren't immutable.


In this example we will see how this happens:

const droid = {
  name: 'R2-D2',
  quality: 'resourceful'
}

// define newDroid with droid reference
const newDroid = droid

// update some attribute
newDroid.quality = 'smart'

// variable droid was affected by that change because it is a reference
console.log(droid)
// [object Object] {
//   name: 'R2-D2',
//   quality: 'smart'
// }

// variable newDroid was updated by the reference
console.log(newDroid)
// [object Object] {
//   name: 'R2-D2',
//   quality: 'smart'
// }

So, non-primitives are copied by their reference.


How to develop in an immutable way


Immutable arrays examples

When we are working with arrays functions some of them mutate the original array, they're:

  • push
  • unshift
  • pop
  • shift
  • sort
  • reverse

Using these array features the result can trigger side effects in your code, however we can do all of these features in another way, doesn't triggering side effects, using an immutable manner.

In the following examples we are going to use the mutate way and after that refactor it, explaining the results and how we can ensure our code will be immutable. All of the following examples will mutate the new array modifying the original one as well.


Working with spread operator in arrays (...)

We are going to use a ES6 feature called 'spread operator' to manipulate and create immutable arrays in a simple way. In the following examples, we will see mutable and immutable manipulation of arrays.


PUSH

This method adds the item to the end of the array changing the original array.


MUTABLE:

const ships = ['X-wing', 'TIE Fighter', 'Millenium Falcon']

// define newShips with ships reference
const newShips = ships

// push 'Destroyer' value to new array created
newShips.push('Destroyer')

// the original variable is updated too, because it is the original reference
console.log(ships)
// ['X-wing', 'TIE Fighter', 'Millenium Falcon', 'Destroyer']

// variable newShips was updated by the reference
console.log(newShips)
// ['X-wing', 'TIE Fighter', 'Millenium Falcon', 'Destroyer']

IMMUTABLE:

const ships = ['X-wing', 'TIE Fighter', 'Millenium Falcon']

// define newShips with a spread of ships, creating a new reference
const newShips = [...ships, 'Destroyer']

// the original variable wasn't updated
console.log(ships)
// ['X-wing', 'TIE Fighter', 'Millenium Falcon']

// only the new one was updated
console.log(newShips)
// ['X-wing', 'TIE Fighter', 'Millenium Falcon', 'Destroyer']

UNSHIFT

This method adds the item to the start of the array changing the original array.


MUTABLE:

const droids = ['R2-D2', 'C-3PO', 'K-2SO']

// define newDroids with droids reference
const newDroids = droids

// add 'BB-8' value to start of the new array created
newDroids.unshift('BB-8')

// the original variable is updated too, because it is the original reference
console.log(droids)
// ['BB-8', 'R2-D2', 'C-3PO', 'K-2SO']

// variable newDroids was updated by the reference
console.log(newDroids)
// ['BB-8', 'R2-D2', 'C-3PO', 'K-2SO']

IMMUTABLE:

const droids = ['R2-D2', 'C-3PO', 'K-2SO']

// put new value in first element, define newDroids with a spread of droids, creating a new reference
const newDroids = ['BB-8', ...droids]

// the original variable wasn't updated
console.log(droids)
// ['R2-D2', 'C-3PO', 'K-2SO']

// only the new one was updated
console.log(newDroids)
// ['BB-8', 'R2-D2', 'C-3PO', 'K-2SO']

POP

This method removes the last element of the array changing the original array.


MUTABLE:

const darkSide = ['Darth Vader', 'Darth Sidious', 'Supreme Leader Snoke']

// define newDarkSide with darkSide reference
const newDarkSide = darkSide

// remove the last element of the new array created
newDarkSide.pop()

// the original variable is updated too, because it is the original reference
console.log(darkSide)
// ['Darth Vader', 'Darth Sidious']

// variable newDarkSide was updated by the reference
console.log(newDarkSide)
// ['Darth Vader', 'Darth Sidious']

IMMUTABLE:

const darkSide = ['Darth Vader', 'Darth Sidious', 'Supreme Leader Snoke']

// remove the last element using slice method, creating a new reference
// in this example we don't need to use spread operator
const newDarkSide = darkSide.slice(0, darkSide.length - 1)

// the original variable wasn't updated
console.log(darkSide)
// ['Darth Vader', 'Darth Sidious', 'Supreme Leader Snoke']

// only the new one was updated
console.log(newDarkSide)
// ['Darth Vader', 'Darth Sidious']

SHIFT

This method removes the first element of the array changing the original array.


MUTABLE:

const lightSide = ['Yoda', 'Luke Skywalker', 'Obi-Wan Kenobi']

// define newLightSide with lightSide reference
const newLightSide = lightSide

// remove the first element of the new array created
newLightSide.shift()

// the original variable is updated too, because it is the original reference
console.log(lightSide)
// ['Luke Skywalker', 'Obi-Wan Kenobi']

// variable newLightSide was updated by the reference
console.log(newLightSide)
// ['Luke Skywalker', 'Obi-Wan Kenobi']

IMMUTABLE:

const lightSide = ['Yoda', 'Luke Skywalker', 'Obi-Wan Kenobi']

// remove the first element using slice method, creating a new reference
// in this example we don't need to use spread operator
const newLightSide = lightSide.slice(1)

// the original variable wasn't updated
console.log(lightSide)
// ['Yoda', 'Luke Skywalker', 'Obi-Wan Kenobi']

// only the new one was updated
console.log(newLightSide)
// ['Luke Skywalker', 'Obi-Wan Kenobi']

SORT/REVERT

These methods reorder the items of the array, changing the original array.


MUTABLE:

const jedis = ['Yoda', 'Luke Skywalker', 'Qui-Gon Jinn', 'Rey']

// define jedisSorted with jedis sorted reference
const jedisSorted = jedis.sort()

// the original variable is updated too, because it is the original reference
console.log(jedis)
// ['Luke Skywalker', 'Qui-Gon Jinn', 'Rey', 'Yoda']

// variable jedisSorted was updated by the reference
console.log(jedisSorted)
// ['Luke Skywalker', 'Qui-Gon Jinn', 'Rey', 'Yoda']

// define jedisReversed with jedis reversed reference
const jedisReversed = jedis.reverse()

// the original variable is updated too, because it is the original reference
console.log(jedis)
// ['Yoda', 'Rey', 'Qui-Gon Jinn', 'Luke Skywalker']

// variable jedisSorted was updated by the reference
console.log(jedisReversed)
// ['Yoda', 'Rey', 'Qui-Gon Jinn', 'Luke Skywalker']

IMMUTABLE:

const jedis = ['Yoda', 'Luke Skywalker', 'Qui-Gon Jinn', 'Rey']

// define jedisSorted with a spread of jedis, creating a new sorted reference
const jedisSorted = [...jedis].sort()

// the original variable wasn't updated
console.log(jedis)
// ['Luke Skywalker', 'Qui-Gon Jinn', 'Rey', 'Yoda']

// only the new one was updated
console.log(jedisSorted)
// ['Luke Skywalker', 'Qui-Gon Jinn', 'Rey', 'Yoda']

// define jedisReversed with a spread of jedis, creating a new reversed reference
const jedisReversed = [...jedis].reverse()

// the original variable wasn't updated
console.log(jedis)
// ['Yoda', 'Rey', 'Qui-Gon Jinn', 'Luke Skywalker']

// only the new one was updated
console.log(jedisReversed)
// ['Yoda', 'Rey', 'Qui-Gon Jinn', 'Luke Skywalker']

Immutable objects examples

When we are working with JavaScript objects, we need to ensure that isn't mutated, because their side effects can be the origin of bugs in our application. Creating some new attribute or modify our value by reference, we could turn the object mutable.


Working with spread operator in objects (...)

To avoid the mutable code, we are going to use a ES6 feature called by 'spread operator'. That feature allow us to create immutable objects in a simple way. In the following examples we will see mutable and immutable manipulation of objects.


MUTABLE

const droid = {
  name: 'R2-D2',
  height: '1.08'
}

// define newDroid with droid reference
const newDroid = droid

// add some new attributes
newDroid.colors = ['blue', 'silver', 'white']
newDroid.productionInformation = {
  manufacturer: 'Industrial Automaton',
  productLine: 'R-series',
  model: 'R2 series astromech droid'
}

// the original object is updated because the reference of newDroid is the same
console.log(droid)
// [object Object] {
//   colors: ['blue', 'silver', 'white'],
//   height: '1.08',
//   name: 'R2-D2',
//   productionInformation: [object Object] {
//     manufacturer: 'Industrial Automaton',
//     model: 'R2 series astromech droid',
//     productLine: 'R-series'
//   }
// }

// newDroid was updated by the reference
console.log(newDroid)
// [object Object] {
//   colors: ['blue', 'silver', 'white'],
//   height: '1.08',
//   name: 'R2-D2',
//   productionInformation: [object Object] {
//     manufacturer: 'Industrial Automaton',
//     model: 'R2 series astromech droid',
//     productLine: 'R-series'
//   }
// }

IMMUTABLE

const droid = {
  name: 'R2-D2',
  height: '1.08'
}

// insted of define a new object with reference, we pass a spread operator of the original object
const newDroid = {
  ...droid,
  colors: ['blue', 'silver', 'white'],
  productionInformation: {
    manufacturer: 'Industrial Automaton',
    productLine: 'R-series',
    model: 'R2 series astromech droid'
  }
}

//the original object is not updated
console.log(droid)
// [object Object] {
//   height: '1.08',
//   name: 'R2-D2'
// }

// we created a new object with the new attributes
console.log(newDroid)
// [object Object] {
//   colors: ['blue', 'silver', 'white'],
//   height: '1.08',
//   name: 'R2-D2',
//   productionInformation: [object Object] {
//     manufacturer: 'Industrial Automaton',
//     model: 'R2 series astromech droid',
//     productLine: 'R-series'
//   }
// }

But we need to be careful with nested objects/arrays of the new object, when you modify nested objects of an object copy, also, the reference in mutation won't be changed in the original object. To resolve this problem we need to pass nested objects with spread as well.


MUTABLE

const droid = {
  name: 'R2-D2',
  height: '1.08',
  colors: ['blue', 'silver']
}

const newDroid = { ...droid }

newDroid.colors.push('white')

//the original object is updated by push of new color
console.log(droid)
// [object Object] {
//   colors: ['blue', 'silver', 'white'],
//   height: '1.08',
//   name: 'R2-D2'
// }

// we created a new object with the same references of nested objects/arrays
console.log(newDroid)
// [object Object] {
//   colors: ['blue', 'silver', 'white'],
//   height: '1.08',
//   name: 'R2-D2'
// }

IMMUTABLE

const droid = {
  name: 'R2-D2',
  height: '1.08',
  colors: ['blue', 'silver']
}

const newDroid = {
  ...droid,
  colors: [...droid.colors, 'white']
}

//the original object is not updated
console.log(droid)
// [object Object] {
//   colors: ['blue', 'silver'],
//   height: '1.08',
//   name: 'R2-D2'
// }

// we created a totally new object
console.log(newDroid)
// [object Object] {
//   colors: ['blue', 'silver', 'white'],
//   height: '1.08',
//   name: 'R2-D2'
// }

Working with rest operator in objects (...)

We can work with 'rest operator' to delete or add some attribute to our object, rest is an ES6 feature, this feature allow us to get remaining properties in an destructuring object and create a new immutable object. In the following example we are going to show how to use rest operator:


MUTABLE

const droid = {
  name: 'R2-D2',
  height: '1.08',
  colors: ['blue', 'silver']
}

const newDroid = droid

delete newDroid.colors

//the original object is updated by deleting colors attribute
console.log(droid)
// [object Object] {
//   height: '1.08',
//   name: 'R2-D2'
// }

// newDroid was updated by reference
console.log(newDroid)
// [object Object] {
//   height: '1.08',
//   name: 'R2-D2'
// }

IMMUTABLE

const droid = {
  name: 'R2-D2',
  height: '1.08',
  colors: ['blue', 'silver']
}

// newDroid is a new variable created using rest operator
const { colors, ...newDroid } = droid

// the original object is not updated
console.log(droid)
// [object Object] {
//   colors: ['blue', 'silver'],
//   height: '1.08',
//   name: 'R2-D2'
// }

// the newDroid object is created without the colors attribute
console.log(newDroid)
// [object Object] {
//   height: '1.08',
//   name: 'R2-D2'
// }

Conclusion

Learning about immutability for the first time will be confusing, but understanding this concept you can build more reliable applications. So, we learned how to manipulate JavaScript objects/arrays in an immutable way (recreating objects/arrays every time), this concept help us to reduce bugs, problems with data manipulation and a better tracking of our code. Talking about's complex manipulation (nested objects/arrays or large objects/arrays) I would like to recommend some libraries, such as ImmutableJS or Immer.

I hope you liked this content! Until next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment