The ‘paradox’ of Angular signals’ effect()s
Changes of ‘unchanged’!
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 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 changes, 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!