Immutable state in NGXS — Part III.

Ivanov Maxim
6 min readJul 15, 2019

--

Introduction

Hello everyone, my name is Maxim Ivanov. I am a member of the NGXS team. Last time we talked about how to create immutable objects to avoid mutation. Today we will get acquainted with the built-in tools in NGXS to prevent mutations.

Immutable, structurally shared data structures are a great paradigm for storing state. Especially when combined with an event-sourcing architecture. However, there is a cost to pay. Creating a new state from the previous one is a boring task in such languages as JavaScript, that don’t support immutability. And still; most of them don’t solve the root problem: lack of language support. For example, where update-in is an elegant concept in a language like ClojureScript, any JavaScript counterparts will basically rely on ugly string paths which are error-prone, hard to type-check and require learning yet another set of API functions by heart to be proficient.

How to avoid mutations state in NGXS?

One of the common challenges with writing code for Redux frameworks is the fact that the state is immutable. When you want to change your state you need to make changes to your own copy of the state and then supply that as the new state.

Mutations can cause unexpected side effects in NGXS

A side effect is anything that has an impact outside of the current scope. Any change to the code base will affect mostly everything if many parts of the code deal with the same reference. The problem of side effects is tightly coupled code, as a consequence. Code fragments interact instead of following the single responsibility rule. By mutating things outside of their scope they have an impact on wider application context and lead to bugs that are difficult to find.

Let’s look at some sample code from an NGXS application:

Note the console output:

unpredictable consequences

Why is the default state [3, 2, 1] instead of [1, 2, 3]?
Why can our state be changed by reference?
It’s very simple, NGXS provides the state to the selectors by reference and does not copy it (as this would have a large negative effect on performance).
The selectors have modified the state!

Furthermore, if we simply change the order of usage in our template:

Now our console has the following output:

unpredictable consequences

Mutation is always risky and has unpredictable consequences.
When a bug is found the following questions are often asked — Where was it changed? What exactly was changed? Who also has access to the reference?
With no history of changes available, these questions can’t easily be answered.

Bugs you don’t know about

We have already observed some strange behavior if your selectors mutate your state, but when using the development mode in NGXS this can get even more strange.

developmentMode: true, production: false

We see that now our state is as it should be, but the result of our selector with mutations doesn’t return anything anymore. It is surprising that we do not see any errors in the console. This is happening because the selector by default does not propagate errors thrown within it but rather return undefined (this behavior is set to change in NGXS v4).

If we switch over to production mode we see errors in the console:

developmentMode: false, production: true

I’m sure that you would not want to catch such errors in production.
What should we do? How to protect ourselves?
Very simple… use immutable objects.

Preventing state mutations in NGXS

Well, we have learned how to create immutable objects in previous articles (shallow copy, deep copy, lazy copy)! How can this be applied in practice? There are several solutions:

NGXS + JavaScript way (shallow copy)

As you can see, this approach is not suitable for deeple nested states! Let’s try a different solution.

NGXS state operators (Immutable Helpers)

This approach uses functional and compositional concepts similar to Ramda but has enhanced type safety because it treats TypeScript as a first-class citizen.

We have improved the readability of the code by introducing declarative updates. Now we can write simple code if we have to work with deep nesting. Note that we still need to use shallow copying for selectors because the state operators are only for updating state, not for creating a read model from the state.

NGXS + Immer

So, what if we stopped fighting the language and embraced it instead?
Can we do this without compromising our immutable data structures?
This is exactly what immer enables us to do…

How does Immer work?

  1. Lazy copy (copy-on-write)
  2. Proxies
How does Immer work?

The green tree is the original state tree. You will note that some circles in the green tree have a blue border around them. These represent proxies. Initially, when the producer starts, there is only one such proxy. It is a draft object that gets passed into your function. Whenever you read any non-primitive value from that first proxy, it will in turn create a Proxy for that value. So that means that you end up with a proxy tree, that kind of overlays (or shadows) the original base tree. This is only for the parts you have visited in the state tree so far.

High level Immer

As soon as you modify a proxy, it creates a shallow clone with your modifications applied in preparation for being re-composed into a new immutable object graph.

Immer-adapter for NGXS

$ npm install @ngxs-labs/immer-adapter immer --save

Our code has become readable and maintainable by adding several decorators! NGXS will throw an error at runtime if you use a developmentMode option even if you forget to add a decorator and start mutating your state.

Preventing a cause of side effects in NGXS

You shouldn’t worry about side effects of mutations anymore, as your states are completely immutable due to ImmutableSelector and ImmutableContext decorators. Your code is getting cleaner.

Benefits

  • Immer enables you to use standard JavaScript data structures and APIs to produce immutable states;
  • Structural sharing out of the box;
  • Object freezing out of the box;
  • Significant boilerplate reduction. Less noise, more concise code.

Resume

  • Use developmentMode in NGXS to track state mutations;
  • Use shallow copy to make the simple immutable states;
  • Use state operators to be as declarative as possible;
  • Use immer-adapter to follow the imperative paradigm.

--

--

Ivanov Maxim

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