loading...
Cover image for Immutability in JavaScript
Zenika

Immutability in JavaScript

yvonnickfrin profile image 🦁 Yvonnick FRIN Originally published at yvonnickfrin.dev Updated on ・5 min read

This article was originally posted on medium here

TLDR; EcmaScript provides utilities to keep data immutable. These tools find their limits with nested structures. immutadot is a library to deal with nested immutable structures. Give it a try.


According to wikipedia an immutable object (unchangeable object) is an object whose state cannot be modified after it is created. This rule is quite simple, if you want to modify some property of an object you have to do it on a copy. We will see a bit later what improvements and fancy features it unlocks for our developments.

EcmaScript

EcmaScript provides utilities to keep our data immutable. Arrays and objects APIs contain methods to create copies and even prevent instances from being updated. More recently EcmaScript introduced a new syntaxes to create copies of objects and arrays.

Object.assign

We want to add a name property in our object.

const lutraLutra = {
  commonNames: ['eurasian otter'],
}
Enter fullscreen mode Exit fullscreen mode

We can do it with Object.assign and a little trick. Basically it copies all properties from an object into another one, thus it mutates the target object. We use a small trick, passing an empty object as first parameter, which creates a new reference.

const newLutraLutra = Object.assign(
  {}, 
  lutraLutra,
  { name: 'Lutra lutra' },
)
Enter fullscreen mode Exit fullscreen mode

We now have a new object with our new name property and a commonNames property remain unchanged. With this method you can create/overwrite multiple properties at the same time.

Array.concat

Now let's do it with an array. We want to add two new elements in an array in an immutable way.

const commonNames = ['eurasian otter']
Enter fullscreen mode Exit fullscreen mode

Unlike Array.push, Array.concat does not mutate our array. Instead, it returns a new array.

const newCommonNames = commonNames.concat(
  'european otter',
  'common otter'
)
Enter fullscreen mode Exit fullscreen mode

This method is flexible. It takes as many elements as you want. They can be either values or arrays.

Object.freeze

Object.freeze isn't really familiar. It lets you make an object immutable! It prevents every type of mutation (creation, modification, deletion) induced by the use of setters.

let lutraLutra = {
  commonNames: ['eurasian otter', 'european otter', 'common otter'],
  name: 'Lutra lutra'
}
Enter fullscreen mode Exit fullscreen mode

We will try to delete name property after object has been frozen.

lutraLutra = Object.freeze(lutraLutra)

delete lutraLutra.name
Enter fullscreen mode Exit fullscreen mode

Reallocation isn't necessary since the object passed as parameter has been made immutable by Object.freeze. This method has two available modes:

  • A non-strict mode that does not apply mutations
  • A strict mode that throws a TypeError if you try to apply mutations

Be careful, it is not recursive. Our property commonNames isn't immutable.

Spread operator

The spread operator syntax has been introduced in ES2015 for arrays and in ES2018 for objects. It copies all properties of a given object into a new object literal.

const newLutraLutra = {
  ...lutraLutra,
  name: 'Lutra lutra',
}
Enter fullscreen mode Exit fullscreen mode

With arrays, it copies all values of an array into a new array.

const newCommonNames = [
  ...commonNames,
  'common otter',
]
Enter fullscreen mode Exit fullscreen mode

it replaces nicely assign and concat, it is easily readable and creates a convention between arrays and objects. It is possible to spread multiple arrays and objects in a same literal.

Why use immutability?

You found out how to make objects and arrays immutable with JavaScript but we didn't explain yet why using immutability is so necessary nowadays. As developers, we are always looking for a way to write more maintainable and readable code. Some paradigms such as Functionnal Programming are focusing on this.

Functional programming’s goal is to allow us to think less and write more descriptive code.

-- Alexander Kondov

It has a declarative approach of programming, which means that you focus on describing what your program must accomplish rather than how it should do it. It gives more meaning to your code so that the next developper can understand it more easily. Functional programming brings along other concepts that help reach this goal, such as immutability.

What are the benefits?

Does it sound like a hype term to you? Immutabilty brings many solutions to programming matters we ecounter everyday:

  • Avoid side effects
  • Data changes detection made simple (shallow comparison)
  • Explicit data changes
  • Memoization
  • Memory optimization
  • Better rendering performances
  • Easy testing

« Unlike most trends in the world of JavaScript, data immutability is bound to stick with us for a while, and for good reason: firstly, because it’s not a trend: it’s a way of coding (and thinking in code) that promotes clarity, ease of use and understanding data flow, and makes code less prone to errors. »

-- Ricardo Magalhães

In the last few years one of our biggest challenges has been to find an efficient way to detect changes in our data to determine whether or not we should render our interfaces. It's easy to detect changes between primitive values, but it's a completely different issue for objects and arrays. You could compare them by value but you would have to implement recursive algorithms and deal with potential issues like cyclical references. Another method would be to compare object references with the strict equality operator ===. It's more effective and there isn't any risk to enter in some deathly infinity loop. That's why modern frameworks enforce this concept.

Highlighted by modern frameworks/libraries

Modern frontend frameworks and libraries are based on a concept that improves drastically performances. This is the well-known Virtual DOM. This technology has been created from a simple evidence: DOM manipulations are expensive.

Like explained, frontend frameworks and libraries chose to use immutability in order to improve their performances. Nowadays we have to deal with more and more data in our applications, and therefore more markups. So our browsers need to handle much more computations than 10 years earlier. DOM operations are expensive, modern frameworks tend to reduce the number of DOM updates.

Why do we need utility libraries?

As we saw earlier, the way to handle immutability in EcmaScript is made simple thanks to syntactic sugar but is quite limited in nested structures. With the arrival of libraries like redux, nested structures have become more popular.

const animals = {
  weasels: {
    lutraLutra: {
      commonNames: ['eurasian otter'],
    },
  },
}

const newAnimals = {
  ...animals,
  weasels: {
    ...animals.weasels,
    lutraLutra: {
      ...animals.weasels.otter,
      name: 'Lutra lutra',
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

As you can see it becomes more tedious to write and harder to read. Simple use-cases like overriding an index of an array aren't easily achievable.

const lutraLutra = {
  name: 'Lutra lutra',
  commonNames: ['eurasian otter', 'european', 'common otter'],
}

const newCommonNames = [...lutraLutra.commonNames]
newCommonNames[1] = 'european otter'

const newLutraLutra = { 
  ...lutraLutra,
  commonNames: newCommonNames,
}
Enter fullscreen mode Exit fullscreen mode

These reasons are sufficient to start finding out some tool that help focusing on what really matters, the meaning of your code. That's why we created immutadot, to help us keep javascript codebase readable and maintenable. Give it a try.

Discussion

pic
Editor guide
Collapse
stevenlangbroek profile image
Steven Langbroek

I gotta say, data-first & lack of auto-currying really hurts their API. Consider:

const person = {
  name: 'Steven'
  cities: ['Berlin', 'Lisbon'],
  currentCity: 'Berlin'
}

If I want to change my current city, as well as push a new one onto the list of cities, I have to do this:

const withNewCity = concat(person, 'cities', 'Amsterdam');
const movedIntoNewCity = set(person, 'currentCity', 'Amsterdam');

Nothing here is reusable or composable. If the methods had taken their data last, and have been curried, we could've had both:

const movedIntoNewCity = compose(
  concat('cities', 'Amsterdam'),
  set('currentCity', 'Amsterdam')
)(person);

Which could be abstract out into:

const moveToNewCity = newCity => compose(
  concat('cities', newCity),
  set('currentCity', newCity)
)

const movedPerson = moveToNewCity('Amsterdam')(person)
Collapse
nlepage profile image
Nicolas Lepage

Hi Steven!

Actually our API has a way to do this:

const { flow, set, push } = require('immutadot')

const person = {
  name: 'Steven',
  cities: ['Berlin', 'Lisbon'],
  currentCity: 'Berlin'
}

const movedIntoNewCity = flow(
  push('cities', 'Amsterdam'),
  set('currentCity', 'Amsterdam')
)(person);

console.log(movedIntoNewCity)

Here's your working example on runkit: runkit.com/embed/kr9e9tins87j

Anyway thank you for your comment, we're waiting for other constructive feedback from you.

Collapse
stevenlangbroek profile image
Steven Langbroek

That's great! Took a cursory look at the code, and if you'd reversed the argument order, you could've delegated to pipe (maybe a more familiar name for people?) and compose (right-associative version of pipe) from lodash, ramda or whatever. The problem now is, what if I'm already piping a bunch of unary functions using any of the bigger FP toolbelts, and add a immutadot function? It won't work:

const newState = pipe(
  over('items', filter(has('id')),
  // won't work
  push('items', newItem)
)(oldState);
Collapse
stevenlangbroek profile image
Steven Langbroek

Good API inspiration, monocle-ts: github.com/gcanti/monocle-ts

const name = Lens.fromPath<Employee>()(['company', 'address', 'street', 'name'])

name.modify(capitalize)(employee)
Collapse
httpjunkie profile image
Eric Bishard

Just trying to understand better. So Redux makes using nested structures more prolific, and nested structures don't work well with immutability (also I claim are hard to read), and immutadot ensures you can keep doing this and stay immutable? I have an honest few questions (because I lack in this area).

Should we be encouraging this nested structure and does immutadot actually help to make it more readable? Does readability still suffer and immutadot just a library to ensure you can keep doing this and bandaids the immutability issue?

Again, this is new territory for me.

Collapse
yvonnickfrin profile image
🦁 Yvonnick FRIN Author

Thank you for your question Eric!

First of all, we’re not encouraging the use of nested structures ; however if you have no choice, or if nested structures are a good fit for your needs, immutadot is here to help.

If you take this example from the article:

const animals = {
  weasels: {
    lutraLutra: {
      commonNames: ['eurasian otter'],
    },
  },
}

const newAnimals = {
  ...animals,
  weasels: {
    ...animals.weasels,
    lutraLutra: {
      ...animals.weasels.otter,
      name: 'Lutra lutra',
    },
  },
}

It can be done with immutadot like this:

import { set } from 'immutadot'

const animals = {
  weasels: {
    lutraLutra: {
      commonNames: ['eurasian otter'],
    },
  },
}

const newAnimals = set(animals, 'weasels.lutraLutra.name', 'Lutra lutra')

It results in a more concise syntax. In my opinion immutadot also brings more meaning to operations by using well-known function names. We are mostly based on ES2015+ so it’s easy to learn.

Hope it helps. Don’t hesitate to ask if you have more questions!

Collapse
httpjunkie profile image
Eric Bishard

That absolutely answers and yes, it is obviously more readable. I guess the next step is for me to try and apply it to some of my own code. So it, in fact, replaces the nested structure with something more declarative and moves the harder to read nested stuff out replaced with something that should be more readable and also probably reduces the possibility of syntax errors in that nested structure. I like it the syntax of set() and how it's still readable!

Collapse
ganeshmani profile image
GaneshMani

Great article. BTW, for React state immutability, i wrote an article
dev.to/ganeshmani/immutablejs-for-...

Collapse
zed_m profile image
Amine Hammou

great post thank you!