Angular Signals in 3 Minutes

Published 10-09-2023

Signals are an amazing addition to Angular. - What makes them so great?

Simplicity.

The majority of the signals interface can be expressed in one small snippet

import {  signal, computed, effect } from '@angular/core';

export class SignalExample {
  // Init
  count = signal(1);

  // Get (Same in Template || Typescript)
  getCount = () => this.count();

  // Setters
  reset = () => this.count.set(1);
  increment = () => this.count.update((c) => c + 1);

  // Computed
  doubled = computed(() => this.count() * 2);

  // Effects
  logCount = effect(() => console.log(this.doubled()));
}

// Omitting untracked & mutate

We can also take a simple example of state, such as count state, and express it in very few lines / concepts compared to its rxjs alternative. i.e. Signals simplify the concepts / lines of code needed to write reactive Angular apps.

Signals

export class SignalCount {
  count = signal(1);
  increment_count() { this.count.update(c => c + 1) }
  log_count = effect(() => console.log(this.count()))
}

RxJS

export class RxjsCount {
  count = BehaviorSubject(1);
  count$ = count.pipe(
    scan((acc, curr) => acc + curr),
    tap((count) => console.log(count)),
    takeUntilDestroyed()
  )
  increment_count() { this.count.next(1) }
  count$.subscribe();
}

Let’s break it down.


Initialization / Get

To create a signal, simply pass a value to signal. This can be anything. Primitive, object, etc… and to get its value, call it like a function.

person = signal({ name: 'erik' });
person(); // → { name: 'erik' }

// Template
{{person()}}

Compare to an Observable, to get the value from an observable we have to:

  • Use async pipe if in a template
  • Use subscribe if in typescript (and remember to take/unsubscribe)

A signal works the same whether we’re in typescript or a template. And we don’t have to handle unsubscribing.

  • Typescript: person()
  • Template: {{person()}}

Setters

There are 2 ways to update a signal. (Which will reactively update anywhere we get the signals value)

We can really do most operations with just set. - update can be viewed as a helpful convenience function.

increment = () => this.count.update((c) => c + 1);
// Or
increment_with_set = () => this.count.set(this.count() + 1)

// Template
<button (click)="increment()">+</button>

Computed

Computed lets us derive a new read-only signal from another signal(s). Computed introduces a new concept known as dependencies.

Dependencies are actually quite simple. Any signal referenced inside of the computed function is a dependency of that computed signal.

A few simple examples below with different data types. Again, any signal referenced in the computed function is tracked as a dependency. When that signal changes, the computed function will re-evaluate.

doubled = computed(() => this.count() * 2)
person_name = computed(() => this.person().name)
first_item = computed(() => this.items()[0])
completed = computed(() => this.items().filter(v => v.completed))

Important - dependencies include signals nested inside of functions.

doubled = computed(() => get_count() * 2)

function get_count() {
  return this.count(); // → `doubled` tracks `count` 
}

Effect

Effects are very similar to computed in the sense that they track dependencies, i.e. track signals referenced in their callback. Effects serve a different purpose than computed. Their purpose is to invoke side effects.

Like computed, effects are re-evaluated when their dependencies change. In the example below, whenever count is updated, logCount logs the new value.

count = signal(1)
logCount = effect(() => console.log(this.count()));

If you’d like to learn about effects in greater details see Angular Signals: Effective Effects

Untracked

An important part of an effect and computed is untracked. Untracked allows you to use a signal inside of an effect/computed without adding that signal as a dependency. i.e. changes to signals inside of untracked will not cause the effect/computed to re-evaluate.

a = signal(1)
b = signal(1)
c = effect(() => {
  const a = this.a();
  untracked(() => console.log(a + this.b()))
});
// → Only logs when `a` changes

Conclusion

That’s it. Looking for more on Signals? Read the rest of the series here.


More Angular Signals

Enjoyed this article? Content similar to this is available on Flotes as studyable Notebooks. Information is delivered in an Anki-style flashcard way, that allows you to fill in blanks and evaluate difficultly, to maximize learning efficiency.

📡 Angular Signals Intro

Mon Oct 09 2023