Angular Components with Extracted Immutable State

Dmitry Tikhonov
ITNEXT
Published in
8 min readApr 20, 2020

--

One of the Angular core concepts is the update of a component’s DOM in case the framework detects changes in its fields used in the template binding expressions. This is very convenient since it allows changing the component’s view just by updating the corresponding fields without any direct manipulations with DOM. At the same time, Angular leaves the way of how the component’s fields are updated to a developer and that works well in some simple scenarios since developers can quickly create components which meet all necessary requirements without the need to introduce any additional abstractions. However, in more complex scenarios, it becomes obvious that the lack of restrictions on the component’s data changes, turns into a problem due to the increased number of side effects which makes the behavior of components less predictable.

There are also additional factors affecting the difficulty of maintaining a free-changing state:

  1. Asynchronous operations

When a component is executing one or several asynchronous operations this means that there are several logical flows changing the shared state, and some synchronization efforts are required. For example, a component loads some data for an input parameter, but when the data is delivered the parameter might have already changed. The data is no longer relevant and, as a result, a new request should be initiated.

2. Component input parameters

In Angular the input parameters might change at any time and in any way (one-by-one or all of them simultaneously) and a well-developed component should correctly reflect the changes.

There is no doubt that Angular creators are aware of these issues, and since the beginning the use of reactive programming (more precisely — RxJs library) has been proposed to resolve them. However, in my opinion, the reactive programing is not always the best solution for typical UI tasks. It is focused on the events distributed over time, but in the majority of cases an immediate reaction to an occurring event is required, and there is another approach (for example used in React) which provides more elegant solution for the above described problem.

The idea of this approach is to separate the component and its data into two different types. In Angular it can be implemented as follows: almost all of the component fields need to be moved into a separate class and only one field left in the component — it is “state” whose type will be that separate class. All of the class fields are supposed to be marked as “readonly”, so the only possible way to change something in the component would be to create a new instance of the class (with changed fields) and then set the instance as component “state”.

A new state instance will be always created as a reaction on events that require the component state to be changed. They can be initiated by user (button click, value change etc.) or by the system (callbacks from setTimeout, “fetch” etc.) As a result, a component lifetime circle can be represented as a series of state instances:

This approach has the following benefits:

1) Performance — all angular template bindings will be able to work with already calculated fields therefore the change detection will be performed extremely fast.

2) Automatic change detection can be entirely disabled since now we know when component data is changed, so there is no need to check that at each mouse movement.

3) Access to a previous state — sometimes, when some field is changed, it is important to know its previous value (for example to clear some cache associated with it) and now we can easily track all the history of the changes.

4) The last component state (before being destroyed) can be stored in some persistent storage and when the component is created again its state can be easily restored.

5) The component’s logic can be fully separated from its presentation and that will simplify unit testing and, to some extent, will help in migration to another framework if needed.

Theoretically all the goals described above can be also achieved in usual Angular components with mutable state, but it will be significantly more difficult.

Another important observation about the state fields is that in most cases there is an internal dependency between them. In the other words it is possible to determine functions whose arguments will be some state fields and results will correspond with the values of other fields (supposed to be present in accordance with the component’s logic).

If such functions cannot be determined that simply means the absence of logic which happens when the component is only used for data presentation.

To explain the idea let’s consider a simple component “Calculator”. It has two inputs for arguments and selector of arithmetic operations:

Arguments and operator can be selected by user or set through component input parameters. Result, arguments and operator should be available as output parameters:

Dependency between the component fields looks as follows:

Most of the time the component state represented by the 8 fields is consistent, but this changes when one of the following 6 events can happens:

1) User has changed the text input for argument #1

2) User has changed the text input for argument #2

3) User has changed the operation selector

4) Component’s input parameter for argument #1 has been changed

5) Component’s input parameter for argument #2 has been changed

6) Component’s input parameter for operation has been changed

*the last three events can happen simultaneously

These events will result in the change of state for some of the component’s fields. This also means that the state is no longer consistent and to make it consistent again corresponding transition functions need to be called, and their results need to be stored in the fields dependent on the recently changed ones. However, these new changes also could lead to inconsistency and further changes will continue happening. Eventually though, all the fields become consistent again (except for the situations with infinite loops which should be considered as bugs) and keep being consistent until a new event happens.

Considering that the only way to make changes in the component’s state is to create a new state instance the reaction on an event can be represented as follows:

Let’s take a closer look at the situation when a user changes the input field:

As shown above, there is always a transition function between a previous state and the next one (the function can be compound). The big advantage of these functions is that they are pure — they receive an immutable state and return an immutable state and are supposed to be independent from invariants, so we can say that we use functional programming in UI development. The only question is how to use this approach in Angular? It can be implemented “manually” but there are several tasks that cannot be easily done in Angular without external libraries:

1) Mapping state fields to Input/Output parameters of Angular component

2) Detecting changes in input parameters

3) Selecting transition functions to be run

To solve the issues, I developed a tiny library “ng-set-state” that helps implementing the approach described above.

Let us look how the calculator can be implemented using the library.

(full source code on stackblitz.com)

First, we need to create a separate class which will represent the component state:

Then the state should be assigned to an Angular component using inheritance from WithStateBase class:

WithStateBase contains “state” property and two public methods to modify it:

· modifyState(prop: (keyof TState), newValue) — which can be called to create a new state instance with one filed changed;

· modifyStateDiff(diff: Partial<TState>) — which can be called when we need to create a new state instance with several fields changed.

The methods can be called from markup (the AoT compilation will check their signatures) to move the component to a new state:

The reaction on input change is more complex and that is why it was moved to a separate method:

You can notice that NewState type alias has been introduced here. Actually it is just a shortcut for Partial<CalculatorState> | null which means that the function returns a subset of the state fields which will be applied by the library to a new state instance. null means no changes are needed.

NOTE: Partial<T> is an example of “mapped types” which were introduced in Typescript 2.+. It is a quite complex concept but when you master it you see how great and helpful it is!

Now we are ready to create transition functions which will be called when the text fields are changed:

It uses @With attribute to indicate which functions should be called to create a new state instance when the library detects changes in the fields whose names listed as the attribute arguments.

NOTE: Typescript checks that the names are actually names of some state fields otherwise there will be a compilation error (“mapped types”).

NOTE: It was not shown in the example, but transition functions can also be asynchronous. You can read about that here.

Now we can display the result:

<h4>Result: {{state.result}}</h4>

Our component does not look not very useful without any Input/Output parameters, so let’s mark some fields as component Input/Output:

NOTE: Static arrays ngInputs and ngOutputs are introduced for Angular Ahead of Time Compilation since it requires all Inputs and Outputs parameter names to be statically resolved.

Now we can use the parameters with the component:

The only thing is left — when arguments are updated form outside (via input parameters) the text in input fields should also be be updated:

The calculator works now, but let’s add some cool thing to it, for example debouncing on user input:

It is very simple but now there is problem — state changing happens out of the angular zone and it cannot detect the changes anymore. That can be fixed by explicit change detection when a new consistent state is applied. To do that we need to add an implementation of onAfterStateApplied() to the component class:

Here we also can show that Angular change detection can be entirely disabled for the component:

Now it is completed, but let’s test that Input/Output works correctly. There is good way to do that — we just need to add the second component instance and bind all its Input/Output properties to the original one:

Here you can check the final version: https://stackblitz.com/edit/angular-calc-demo-ngsetstate

I have been using the approach described in the article for the last year, and it proved very helpful when I needed to develop some complex components. It is, of course, not the only way of how the development process can be improved — there are other great libraries like MobX or NgRx which can also be useful for managing the growing complexity.

--

--