DEV Community

Cover image for Angular Signals: Keeping the Reactivity Train
Evgeniy OZ
Evgeniy OZ

Posted on • Edited on • Originally published at Medium

Angular Signals: Keeping the Reactivity Train

There is a very underrated tweet by Pawel Kozlowski:

In this article, let’s apply examples from that MobX article to Angular Signals.

In the examples, I will prefix variables containing signals with the $ symbol: $itIsSignal. Variables and fields without this prefix are not signals. After reading this article, you might agree that it helps to see what should be called as a function to be read (and observed). Or you might decide not to use this convention — it’s up to you, I don’t insist :)

Examples are “converted” from the MobX article examples, but I’ll replace the term “dereferencing” with “reading” and “tracking function” with “watching function” (or just watcher()), because in Angular Signals, a signal needs to be read inside a watching function to become observed.

Right now, Angular has two “watchers” implemented: the effect() function and the templates. They are implementing the same role in Angular Signals reactivity, so I’ll use watcher() to reference both of them.

Let’s start already:

import { signal, WritableSignal } from "@angular/core";

type Author = {
  $name: WritableSignal<string>;
  age: number;
};

class Message {
  $title: WritableSignal<string>;
  $author: WritableSignal<Author>;
  $likes: WritableSignal<string[]>;

  constructor(title: string, author: string, likes: string[]) {
    this.$title = signal(title);

    this.$author = signal({
      $name: signal(author),
      age: 25,
    });

    this.$likes = signal(likes);
  }

  updateTitle(title: string) {
    this.$title.set(title);
  }
}

let message = new Message('Foo', 'Michel', ['Joe', 'Sara']);
Enter fullscreen mode Exit fullscreen mode

✅ Correct: reading inside the watching function

watcher(() => {
  console.log(message.$title());
});

message.updateTitle('Bar');
Enter fullscreen mode Exit fullscreen mode

Here, we’ll receive the expected update in the console because watcher() has read the $title, and after that, it will re-read this signal when receive an update notification.

❌ Incorrect: changing a non-observable reference

watcher(() => {
  console.log(message.$title());
});

message = new Message('Bar', 'Martijn', ['Felicia', 'Marcus']);
Enter fullscreen mode Exit fullscreen mode

Here, we replace the message, but watcher() uses the reference to another variable, and it will not be notified that we’ve replaced the reference.

❌ Incorrect: reading a signal outside of the watching function

const title = message.$title();

watcher(() => {
  console.log(title);
});

message.updateTitle('Bar');
Enter fullscreen mode Exit fullscreen mode

Here, title is not a signal. It’s the value we’ve read outside of the watching function, so watcher() will not be notified when we update the signal.

✅ Correct: reading inside the watching function

watcher(() => {
  console.log(message.$author().$name());
});

message.$author().$name.set("Sara");

message.$author.set({
  $name: signal("Joe"),
  age: 35,
});
Enter fullscreen mode Exit fullscreen mode

In the watcher() function, we read both $author and $name. Therefore, every time we update either of them, the watcher() will be notified.

❌ Incorrect: store a local reference to a watched signal without reading

const author = message.$author();

watcher(() => {
  console.log(author.$name());
});

message.$author().$name.set("Sara");

message.$author = signal({ $name: signal("Joe"), age: 30 });
Enter fullscreen mode Exit fullscreen mode

The first change will be picked up, message.$author() and author are the same object, and the .$name property is read in the watcher().

However, the second change is not picked up, because the message.$author relation is not tracked by the watcher(). The watcher() is still using the “old” author.

🛑 Common pitfall: console.log

watcher(() => {
  console.log(message);
});

// Won't trigger a re-run.
message.updateTitle("Hello world");
Enter fullscreen mode Exit fullscreen mode

In the above example, the updated message title won’t be printed because it is not read inside the watcher(). The watcher() only depends on the message variable, which is not a signal but a regular variable. In other words, $title is not read by the watcher().

✅ Correct: updating the objects

watcher(() => {
  console.log(message.$likes().length);
});

message.$likes.mutate(likes => likes.push("Jennifer"));

message.$likes.update(likes => ([...likes, "Jennifer"]));
Enter fullscreen mode Exit fullscreen mode

This will work as expected: in Angular Signals, calling the mutate() method will always result in an update notification.

If an Angular Signal contains an object, the new value will always be considered unequal to the previous value by default (unless you override the equality check function). As a result, the second line will also trigger an update notification.

❌ Incorrect: mutating an object inside a signal

watcher(() => {
  console.log(message.$author().age);
});

message.$author().age = 23;
Enter fullscreen mode Exit fullscreen mode

We’ve updated the field of an object, but we didn’t call any method that could cause a notification — the signal will not be aware of our actions, and will not compare values, will not emit notifications.

❌ Incorrect: reading signals asynchronously

watcher(() => {
  setTimeout(() => {
    console.log(message.$likes().join(", "));
  }, 10);
});

message.$likes.mutate(likes => likes.push("Jennifer"));
Enter fullscreen mode Exit fullscreen mode

In Angular Signals, the watching functions are unable to detect reactivity graph dependencies within asynchronous calls.

Conclusion

Using the experience accumulated by other frameworks and knowledge from our own experiments, we can fully unleash the power of automatic dependency tracking in Angular Signals without derailing the “reactivity train”.


💙 If you enjoy my articles, consider following me on Twitter, and/or subscribing to receive my new articles by email.

🎩️ If you or your company is looking for an Angular consultant, you can purchase my consultations on Upwork.

Top comments (0)