avatarPanos Zafeiropoulos

Summary

The article discusses a "paradox" in Angular signals where an "unchanged" array of objects signal triggers an effect due to the presence of another, changed, simple number signal within the same effect block.

Abstract

The author of the article highlights a nuanced behavior in Angular signals, specifically when tracking changes in an array of objects. The default signal equality function in Angular 17 and later versions uses a static object comparison, which does not detect changes in the properties of objects within an array. However, when multiple signals are read within an effect() block and one of them changes, Angular's mechanism triggers the effect again, even if the array of objects signal is technically "unchanged." This can lead to unexpected updates and a perception of "changes of unchanged" data, which the author suggests is counterintuitive and should be addressed with clearer documentation in future Angular releases.

Opinions

  • The author believes that the current behavior of Angular signals, where an effect can be triggered by an "unchanged" array of objects due to the presence of another changed signal, is not consistent and can be confusing.
  • It is the author's opinion that Angular's documentation should be updated to provide more insight into this behavior to help developers understand and handle such cases effectively.
  • The author suggests that using a custom signal equality function can resolve issues with detecting changes in array objects' properties, indicating a potential workaround for the observed inconsistency.
  • The article implies that while signals in Angular offer a cleaner approach to change tracking compared to observables, developers need to be aware of subtle behaviors that could affect their applications' expected outcomes.

The ‘paradox’ of Angular signals’ effect()s

Changes of ‘unchanged’! — Not an obvious behavior of an array of objects signal.

Intro

Dozens of posts out there have been written about signals, the new Angular way of tracking changes in values. And they keep growing.

Indeed, in many cases, signals offer a neater way to track value changes than the observables. However, in this post, I am not going to go into a detailed presentation of Angular signals. I want just to point out a not-so-clear behavior of sensing signal changes in the effect() ‘operation’.

The case

Consider we have defined a signal of an array of objects in a service, e.g.:

. . .
public $formFields = signal< IFormField[]>(this.formFields);          // The Signal
. . .

Whenever there are some changes in the fornFields array, we can update or set the new value to the $formFields signal, e.g.:

. . .
this.$formFields.update(()=>this.formFields);     // Signal update 
. . .

or

. . .
this.$formFields.set(this.formFields);   // Set Signal value
. . .

Then in a “consumer”, e.g. in a component’s constructor, we can use the effect() to monitor the signal changes, e.g.:

  constructor() { 
    effect(()=> {
      this.formFields = this.itemService.$formFields();
      console.log('>===>> formFields', this.formFields);
    });
  }

As you have probably suspected we cannot sense a change of a property value in a member object of the array. And this is also the expected behavior, because of the default signal equality function [see more at official documentation here]

Thanks to OZ post here, the reason for this behavior, is that Angular 17 (Angular v17.next.8+ and afterward) uses just the static object.js method as the default equality function to compare whether a new signal value is actually different than the previous one.

Well, if we provide our custom signal equality function, then the changes are “sensed’ OK.

But here, in my opinion, there is a not-so-consistent behavior.

Let’s say we leave the default signal equality function (the one that does not sense changes in particular property values of array objects), but then, we add another signal of a simple number, e.g.:

In our service:

// The Signals
. . .
public $formFields = signal< IFormField[]>(this.formFields);          
public $item = signal<number>(0)
. . .

. . .
  . . .
// Set Signals values
this.$formFields.set(this.formFields);   
this.$item.set(1) 
. . .

In our component

  constructor() { 
    effect(()=> {
      
      this.item = this.itemService.$item()!;
      console.log('>===>> item', this.item);
      
      this.formFields = this.itemService.$formFields();
      console.log('>===>> formFields', this.formFields);

    });
  }

You might expect that only the change in $item signal has been sensed. But this is not the case. The array of the $formFields signal has been also updated this time!

This happens because as the official documentation states: “When an effect runs, it tracks any signal value reads. Whenever any of these signal values change, the effect runs again.” So, if the change of the $item is captured, then it seems that the Angular signals mechanism also fetches the other signals inside the effect() block (i.e.: the $formFields array of objects). And this happens regardless the array of objects is considered “unchanged”, as we have previously seen when it was alone in the block.

And this is a bit confusing and seems a kind of paradox “changes of unchanged”.

Conclusion

In conclusion, I’d like anyone to mind this when dealing with similar cases. Furthermore, the Angular documentation should be updated and enlightened a bit at this point in future stable releases of signals.

That’s it! Thanks for reading!

Angular Signals
Angular
Recommended from ReadMedium