Angular Signals: Keeping the Reactivity Train
Tags: angular, reactivity, webdev, javascript
Published: 2023-08-19 12:42:28

There is a very underrated tweet by Pawel Kozlowski:
{% embed https://twitter.com/pkozlowski_os/status/1543246502449106947 %}

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:

```ts
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']);
```

## ✅ Correct: reading inside the watching function

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

message.updateTitle('Bar');
```

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

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

message = new Message('Bar', 'Martijn', ['Felicia', 'Marcus']);
```

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
```ts
const title = message.$title();

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

message.updateTitle('Bar');
```

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
```ts
watcher(() => {
  console.log(message.$author().$name());
});

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

message.$author.set({
  $name: signal("Joe"),
  age: 35,
});
```

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

```ts
const author = message.$author();

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

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

message.$author = signal({ $name: signal("Joe"), age: 30 });
```

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
```ts
watcher(() => {
  console.log(message);
});

// Won't trigger a re-run.
message.updateTitle("Hello world");
```

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
```ts
watcher(() => {
  console.log(message.$likes().length);
});

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

message.$likes.update(likes => ([...likes, "Jennifer"]));
```

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
```ts
watcher(() => {
  console.log(message.$author().age);
});

message.$author().age = 23;
```
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
```ts
watcher(() => {
  setTimeout(() => {
    console.log(message.$likes().join(", "));
  }, 10);
});

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

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](https://twitter.com/eugeniyoz), and/or [subscribing](https://medium.com/@eugeniyoz) 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](https://www.upwork.com/services/consultation/development-it-evgeniy-1696285343741362176?ref=project_share)._