Dependency Graph in Angular Signals
Tags: angular, signals, reactivity
Published: 2023-07-29 16:00:50

When you use RxJS observables, to start receiving emitted values from an observable, the listener should explicitly subscribe to that observable. When you use Angular Signals, this is handled implicitly by a dependency tracking mechanism.

## Producers, Consumers
The first time when you read a signal value inside the function, passed to `computed()` or `effect()`, or read it in a template, new consumer-producer edges will be created in the dependency graph.

To better understand the terms **producer** and **consumer**, let’s take a look at this example:

```ts
const $isLoading = signal<boolean>(false);
const $isSaving = signal<boolean>(false);

const $isBusy = computed<boolean>(() => {
  return $isLoading() || $isSaving();
});
```

Here `$isLoading`, `$isSaving`, and `$isBusy` are **producers** [of reactivity], and the function, passed to `computed()`, is a **consumer** — it consumes the reactivity, created by producers. This function exists to create a consumer, `$isBusy`, and this fact makes `$isBusy` a consumer and a producer simultaneously (and that’s ok).

Every producer knows its consumers, while consumers are aware of all of the producers on which they depend.

The body of the function passed to `computed()` in our example is called “reactivity context”.

`$isLoading` and `$isSaving` are consumed in a reactivity context, so they are automatically registered as producers for `$isBusy`, and now `$isLoading` and `$isSaving` will know `$isBusy` as their consumer.

## Dependency Graph
Dependencies are tracked automatically and recursively, information about the dependencies creates the **Dependency Graph**. The size of the graph depends on the number of signals in your application.

Let’s take a look at this dependency graph:

![Graph](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1pb3hkrlpnkwrpdi9jto.gif)

In this example, C1 and C6 are consumers only (template and `effect()`).

C2, C3, C4, C5, C7, and C8 are signals, created with `computed()` — they are consumers and producers at the same time.

P1-P6 are regular writeable signals, they are producers only.

When P2 will produce a value, C2 and C1 will be notified.

When P4 will produce a value, only C6 will be notified.

P6 will notify C4, C3, C2 and C1.

P7 will notify every consumer.

We don’t have to call `subscribe()` or add observables to `combineLatestWith()` or `withLatestFrom()`. We don’t even have to care about unsubscribing — the edges will be removed automatically from the dependency graph when referenced producers or consumers are not being used anymore and are destroyed by the garbage collector.

But if we’ll complicate our example a little, we’ll find some interesting (and not so obvious) consequences of implicit (automatic) dependency tracking:

```ts
export class ExampleService {
  private $audioListener = signal<AudioListener | undefined>(undefined);
  
  constructor() {
    window.document.addEventListener('click', () => {
        const audioListener = new AudioListener()
        this.$audioListener.set(audioListener);
      }, { once: true, passive: true });
  }

  getAudioListener(): AudioListener | undefined {
    return this.$audioListener();
  }
}
```

```ts
export component ExampleComponent {
  private service = inject(ExampleService);
  
  private $hoverAudio = computed(() => {
    const hoverAudioBuffer = this.$hoverAudioBuffer();
    if (hoverAudioBuffer) {
      // ⬇️
      const listener = this.service.getAudioListener();
      // ⬆️
      if (listener) {
        const hoverAudio = new PositionalAudio(listener);
        hoverAudio.setBuffer(hoverAudioBuffer);
        return hoverAudio;
      }
    }
    return undefined;
  });
}
```

The line, where we are getting a value for the `listener` variable, doesn’t read a signal’s value explicitly, but it calls a function that reads a value from the signal `$audioListener`. And still, this dependency will be added to the dependency graph! `$audioListener` will be registered as a producer for `$hoverAudio` signal. `$hoverAudio` will be registered as a consumer for `$audioListener`. It doesn’t matter how many levels of function we’ll call — dependency will be correctly registered.

There are pros and cons to this nested implicit tracking, as with any automated and implicit mechanism.

This example is a very simplified code from a real app I’m working on, and it was very helpful that I don’t have to expose the signal itself and can just expose a getter, and dependency tracking will do the rest.

But I realize that in some moments I might create a dependency non-intentionally. It might lead to non-needed updates and additional memory consumption.

We can control it:

```ts
private $hoverAudio = computed(() => {
  const hoverAudioBuffer = this.$hoverAudioBuffer();
  if (hoverAudioBuffer) {
    // ⬇️                ⬇️
    const listener = untracked(() => this.service.getAudioListener());
    // ⬆️                ⬆️
    if (listener) {
      const hoverAudio = new PositionalAudio(listener);
      hoverAudio.setBuffer(hoverAudioBuffer);
      return hoverAudio;
    }
  }
  return undefined;
});
```

But to do that, you should know the internals of `getAudioListener()`. Also, a function that wasn’t reading from a signal today, might start reading it tomorrow.

There is one limitation in automated tracking: producers, consumed asynchronously, will not be registered:

```ts
const $isLoading = signal<boolean>(false);
const $isSaving = signal<boolean>(false);

const $isBusy: Signal<boolean> = computed(() => {
  let isSaving;
  setTimeout(() => isSaving = $isSaving(), 0);
  return $isLoading() || isSaving;
});
```

In this example, `$isSaving <-> $isBusy` edge will not be added to the graph.

Angular Signals are young, we need at least a couple of years to completely understand if the pros of automatic dependency tracking outweigh their cons, but right now I do like this feature — it helps me most of the time, and I use `untracked()` quite rarely.

---
💙 _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)._