Immutable state in NGXS — Part II.

Ivanov Maxim
5 min readJul 15, 2019

--

Introduction

Hello everyone, my name is Maxim Ivanov. I am a member of the NGXS team. In the previous article we looked at how we can prevent object mutations. Now we will look at how you can avoid mutations by creating immutable objects and making ‘changes’ to them in an immutable way.

What is immutability?

Immutable objects are objects whose state can not be changed once the object is created. An immutable value is one that, once created, can never be changed. In JavaScript primitive values such as numbers, strings and booleans are always immutable. However, data structures like objects and arrays are not. Treating all objects as immutable leads to great advantages in the ability to reason about your code, resulting in much simpler application development (especially for debugging your program). Unfortunately this comes with a new challenge:

How do we create a new version of an immutable object with the desired changes applied to it?

Ways to create immutable objects

Although JavaScript doesn’t support for immutable objects, we can still write our code so as to avoid most mutations. In order to create a new version of an object with changed values, we need to start by copying the existing object. Copying is a basic operation but has subtleties that can have significant overhead. As objects are made up of more and more parts, copying becomes nontrivial.

Several strategies exist to treat this problem:

Shallow copy

A shallow copy is to make a bitwise copy of an object. A new object is created that has an exact copy of the values in the original object. If any of the fields of the object are references to other objects then just the reference is copied i.e., only the memory address is copied.

TypeScript (target: es5)

In fact, your code is always compiled (target: es5) into old ECMAScript for support by all browsers.. Therefore we can notice this:

JavaScript (es5)

Spread syntax effectively goes one level deep while copying an array. Therefore, it may be unsuitable for modifying hierarchical objects.

Deep copy

A deep copy copies all fields, and makes copies of dynamically allocated memory pointed to by the fields. A deep copy occurs when an object is copied along with the objects to which it refers.

One has to be really careful with this JSON approach! It doesn’t work with values not compatible with JSON. For example, the following code will have unexpected results:

The JSON format per se doesn’t support object references (although an IETF draft exists), hence JSON.stringify() doesn't try to solve them and fails accordingly. Let’s look at the code below:

JSON.stringify returns the JSON-safe string representation of its input parameter. Note that non-stringifiable fields will be silently stripped off.
Why aren’t all values stringifiable? …
Because JSON is a language agnostic format.

Let’s compare to an alternative:

This copy function is a recursive function that copies an object’s properties while handling the cases when the value is a simple object or an array.

As you can see, copying objects is not a trivial task. Both options above may be good or bad under certain circumstances. Rather consider using a library function if you have to work with such data.

Lazy copy (copy-on-write)

In shallow copy, only fields of primitive data type are copied while the objects references are not copied. Deep copy involves the copy of primitive data type as well as objet references. There is no hard and fast rule as to when to do shallow copy and when to do a deep copy.

A lazy copy can be defined as a combination of both shallow copy and deep copy. The mechanism follows a simple approach — at the initial state, shallow copy approach is used. A counter is also used to keep a track on how many objects share the data. When the program wants to modify the original object, it checks whether the object is shared or not. If the object is shared, then the deep copy mechanism is initiated.

Lazy copy looks to the outside just as a deep copy, but takes advantage of the speed of a shallow copy whenever possible. The downside are rather high but constant base costs because of the counter. Also, in certain situations, circular references can cause problems.

Although JavaScript doesn’t support lazy copy, we can still take this approach by leveraging external libraries: Immer, Immutable JS, Ramba.

Let’s compare some of these:

  • Native (without lazy-copy)
  • Ramda: Functional approach through composition
  • Immer: Record proxy object mutations and apply in an immutable way

ImmutableJS: provides many Persistent Immutable data structures including: List, Stack, Map, OrderedMap, Set, OrderedSet and Record.

Summary

  • Objects are mutable by default;
  • Objects are compared by reference and have unique identities;
  • Variables hold references to objects;
  • Primitives are immutable;
  • Primitives are compared by value, they don’t have individual identities;
  • Simple immutable states can be achieved using shallow copy;
  • Do not use deep copying, as it is bad for performance.

--

--

Ivanov Maxim

Code 🤖, Bike 🚵 and Music 🎶 Teams: @splincodewd ★ @Angular-RU ★ @ngxs ★ github.com/splincode