Angular Components State Tracking with ng-set-state

Dmitry Tikhonov
ITNEXT
Published in
8 min readApr 18, 2021

--

In the previous article(“Angular Components with Extracted Immutable State”), I explained why changing component fields without any restrictions is not always good, and also presented a library that allows you to streamline a component state changes.

Since then, I’ve slightly modified its concept and made it easier to use. This time I’ll focus on a simple (at first glance) example of how it can be used in scenarios where rxJs usually would be needed.

Concept

There is an immutable object that represents all field values of some component:

Each time when some field value (or several field values) of the component is changed, then a new immutable object will be created. It will contain all the old unchanged values and the new ones:

By comparing these two objects, it can be determined what exact fields values have changed and if there are logical dependencies on these fields then the corresponding value evaluation should be performed. After the evaluation is done then a 3-rd object will be created and this 3-rd object will contain the original values and the newly evaluated ones.

Now we have the new object which also can be compared with the previous one and next dependencies can be evaluated if it is required. This scenario can be repeated many times until we have a fully consistent object. After that angular will render its data. Profit!

Simple Greeting Form

Let’s create a simple greeting form (source code on stackblitz):

Obviously, the greeting field depends on the userName field and there are several ways to express the dependency:

  1. Converting greeting to a property with a getter, but in this case its value will be calculated on each change detection.
  2. Converting userName to a property with a setter which will update the greeting filed value.
  3. Creating an event handler on “(ngModelChange)” but it will make the markup more complex;

They will work, but if some other field depends on greeting (for example, “greetings counter”) or “greeting” depends on more than one field (e.g. greeting=f(userName, template) ), then none of these methods will help, so another approach is proposed:

First, the component should be decorated with @StateTracking decorator, or alternatively initializeStateTracking function should be called in the constructor (custom component decorators do not properly works in some old Angular versions):

@StateTracking decorator (or initializeStateTracking function) finds all the component fields which other fields might depend on and replaces them with properties with getters and setters, so that the library can keep track changes.

Next, some transition function should be defined e.g.:

Each transition function receives an object that represents a current component state and should return an object which will contain only updated fields. It will be merged into a copy of the current state object. Then the new updated copy will become a new component state.

Optionally, you can add a second argument which will receive a previous state object.

If you define a third parameter, it will receive a “difference” object between the current and previous states:

ComponentState and ComponentStateDiff are typescript mapped types which filter out methods and event emitters. Also ComponentState marks all fields as readonly (state is immutable) and ComponentStateDiff marks all fields as optional, since transition function can return any subset of the original state.

For simplicity let’s define the type aliases:

Decorator @With receives a list of field names whose value changes will trigger the decorated static (!) method. Typescript will check that the class has the declared fields and that the method is static (transitions should “pure”)

Tracking log

Now the form displays a corresponding greeting when user types a name. Let’s see how the component state changes:

onStateApplied is a hook function which is called each time when the component state becomes consistent — that mans that all transition functions have been called and no more changes detected:

Transition:
{} =>
{"userName":"B","greeting":"Hello, B!"}
Transition:
{"userName":"B","greeting":"Hello, B!"} =>
{"userName":"Bo","greeting":"Hello, Bo!"}
Transition:
{"userName":"Bo","greeting":"Hello, Bo!"} =>
{"userName":"Bob","greeting":"Hello, Bob!"}

As we can see, the component goes to a new state every time user types a next character and the greeting field is updated immediately. If it needs to prevent the greeting from updating on every name change then it can be easily accomplished by adding Debounce extension to the @With decorator:

Now the library waits 3 second after the last name change and only then executes the transition:

Transition:
{} =>
{"userName":"B"}
Transition:
{"userName":"B"} =>
{"userName":"Bo"}
Transition:
{"userName":"Bo"} =>
{"userName":"Bob"}
Transition:
{"userName":"Bob"} =>
{"userName":"Bob","greeting":"Hello, Bob!"}

Let’s add an indication that the form is waiting:

It seems working but an issue appears — if a user started typing and then decided to return the original name during the given 3 seconds, then from the “greet” transition perspective nothing has changed and the function will never called and the form will keep “thinking” forever unit you type a different name. It can be solved by adding @Emitter() decorator for userName field:

@Emitter()
userName: string;

which says to the library, that any assignment of any value to this field will be considered as a change, regardless of whether the new value is the same as the previous one.

However, there is another solution — when the form stops thinking, it can set userName to null and the user will have to start typing a new name:

Now let’s think about a situation when a user is impatient and wants to get the result immediately. Well, let’s let him press Enter ((keydown.enter)="onEnter()") to get the immediate result:

It would be nice to know how much time to wait if user does not press Enter — some kind of countdown counter would be very useful:

<h1 *ngIf="isThinking">Thinking ({{countdown}} sec)...</h1>

and it is how it looks like:

The countdown is also should be reset each time a new greeting is ready. It prevents the situation when the user immediately presses [Enter] and the countdown remains 3 at this moment — after that it stops working, since its value will never change again. For simplicity lets reset all the fields that depend on “Is Thinking” flag:

Change Detection

Obviously, the countdown woks asynchronously and this fact does not cause any issue with the Angular change detection as long as the component detection strategy is Default. However, if the strategy is OnPush, then nothing can tell the component that its state is changing while the countdown is in progress.

Fortunately, we’ve already defined a callback function which is called each time when the component state has just changed, so the only thing which is required is adding an explicit change detection there:

Now it works as expected even with OnPush detection strategy.

Output Properties

The library detects all component event emitters and calls them when a bound component field has just changed. By default the binding is performed using Change suffix for the event emitters:

greeting:  string;@Output()
greetingChange = new EventEmitter<string>();

Shared State

Typically, when a component is destroyed (e.g. hidden by *ngIf) then all still pending asynchronous operations initiated by that component end in nothing. However, the library allows extracting a component state with its transitions into a separate object which can exist independently on the component. More other, such object can be shared between several components simultaneously!

Let’s convert the greeting form component into a service:

and add it to the module providers.

includeAllPredefinedFields means that all fields that have some initial value (even if it is null) will be automatically included into state objects.

To utilize the service inside a component, follow these steps:

  1. Inject the service instance into the component
  2. Point the state tracker to the service instance
  3. Declare component fields which will be bound to the corresponding service field
  4. Subscribe to the service state changes — it is necessary when the explicit change detection is required or change detection strategy is “On Push”

After these steps have been taken, the component will look like this:

The service instance is passed into initializeStateTracking function (using @StateTracking() decorator is also possible but it would take more efforts) which returns a state tracking handler that provides some methods to control the tracking behavior.

The subscription is required to call onStateApplied callback function when the service state is changed or to call a local transition function which depends on the shared state. If a component uses the default change detection strategy and there are no local transition functions, then subscription is not required.

Do not forget to unsubscribe it when the component is destroyed to avoid memory leaks. Alternatively, you can call handler.release() or releaseStateTracking(this) functions to release component subscriptions, but these methods also cancel pending asynchronous operations, which is not always desirable.

Complex Shared State

The library allows using shared states not only in components, but also in others services.

Let’s create a service which will will log all greetings and simulate sending them to a server:

On each greeting field value change it will be added to log array. logVersion will be increased each time the array is changed to provide change detection without making log array immutable:

The service will not send new greetings immediately, it will wait some time to collect a bunch of changes:

And finally saving:

This transition function differs from the others — it is asynchronous.

  1. It decorated with WithAsync instead of just With.
  2. The decorator has a concurrent launch behavior specification (OnConcurrentLaunchPutAfter in this case)
  3. Instead of a current state object it receives a function which returns a current state at the moment it called.

In the same way, we can implement greeting deleting and restoring , but I will skip this part, since there is nothing new in it.

As a result, our form will look like this:

We’ve just reviewed an example of a user interface with a relatively complex asynchronous behavior. However, it turns out that implementing this behavior is not that difficult using the concept of a series of immutable states. At least it can be considered as an alternative to RxJs.

  1. Link to the code: https://stackblitz.com/edit/set-state-greet

2. Link to the previous article: Angular Components with Extracted Immutable State

3. Link to ng-set-state source code: https://github.com/0x1000000/ngSetState

--

--