Skip to content

Angular Signals

Signals are Angular's built-in reactive primitive, a wrapper around a value that notifies interested consumers whenever it changes. This lesson covers signal(), computed(), and effect() in depth.

What Is a Signal?

A signal is a reactive container for a value. You read it by calling it as a function (count()), and update it with .set() or .update(). Angular automatically tracks which parts of a template (or which computed()/effect()) depend on a signal, and updates only what's actually affected when it changes.

Signals were introduced to give Angular fine-grained, predictable reactivity, closer to how libraries like SolidJS or Vue's Composition API work, as an alternative (and complement) to Zone.js-based change detection and RxJS-heavy patterns.

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

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count() }}</p>
    <p>Doubled: {{ doubled() }}</p>
    <button (click)="increment()">+1</button>
  `,
})
export class CounterComponent {
  count = signal(0);
  doubled = computed(() => this.count() * 2);

  increment() {
    this.count.update(value => value + 1);
  }
}

doubled automatically recalculates whenever count changes, no manual synchronization code needed.

The Three Core Signal APIs

const count = signal(0);
count();          // read
count.set(5);       // overwrite
count.update(v => v + 1); // update based on current value

const doubled = computed(() => count() * 2); // derived, read-only

effect(() => {
  console.log('count changed to', count());  // side effect
});
  • signal(initialValue) creates a writable signal; call it like a function to read the current value.
  • .set(value) replaces the value entirely; .update(fn) computes a new value from the current one.
  • computed(fn) creates a read-only signal derived from other signals, recalculated lazily and only when needed.
  • effect(fn) runs a side effect automatically whenever any signal read inside it changes.

Signals Cheatsheet

Core signal APIs and related helper functions.

API Example Purpose
signal(value) count = signal(0); Create a writable reactive value
Read count() Read the current value
.set() count.set(5) Replace the value
.update() count.update(v => v + 1) Compute a new value from the current one
computed() computed(() => count() * 2) Derive a read-only value from other signals
effect() effect(() => console.log(count())) Run a side effect on change
.asReadonly() count.asReadonly() Expose a signal without write access
input() value = input(''); Signal-based component input
toSignal() toSignal(obs$) Convert an Observable to a signal

Signals vs Plain Class Properties

A plain class property changing doesn't automatically tell Angular anything, Angular's change detection has to re-check the whole component (or rely on Zone.js patching async APIs) to notice. A signal explicitly tracks who reads it, so Angular can update exactly the parts of the DOM that depend on it, more precisely and efficiently.

computed() Is Lazy and Memoized

A computed() signal doesn't recalculate on every read, it recalculates only when one of its dependencies has actually changed since the last read, and caches the result in between. This makes even expensive derived computations cheap to read repeatedly.

const expensive = computed(() => {
  console.log('recalculating...');
  return heavyComputation(count());
});

expensive(); // logs 'recalculating...', computes
expensive(); // no log, returns the cached value (count() hasn't changed)

Using effect() Correctly

effect() is meant for side effects, not for computing derived state (use computed() for that). Effects also support a cleanup function for canceling ongoing work, similar in spirit to ngOnDestroy but scoped to a single effect.

effect((onCleanup) => {
  const id = setInterval(() => console.log(count()), 1000);
  onCleanup(() => clearInterval(id));
});

Signals With input() and HttpClient

Signals integrate naturally with the modern input() function and with RxJS via toSignal(), letting a component's entire reactive graph, inputs, derived values, and fetched data, be expressed with signals end to end.

export class UserCardComponent {
  userId = input.required<string>();
  private userService = inject(UserService);

  user = toSignal(
    toObservable(this.userId).pipe(switchMap(id => this.userService.getUser(id)))
  );
}

Common Mistakes

  • Calling .update() with logic that has side effects, updater functions should be pure and only compute a new value.
  • Using effect() to compute a value that should just be a computed() signal.
  • Forgetting the parentheses when reading a signal (count instead of count()), which passes the signal function itself, not its value.
  • Mutating an object or array stored in a signal in place instead of calling .set()/.update() with a new reference.

Key Takeaways

  • Signals are reactive containers read by calling them as functions and updated with .set()/.update().
  • computed() derives read-only values lazily and memoizes them until a dependency actually changes.
  • effect() runs side effects automatically in response to signal changes, not for computing new state.
  • Signals integrate with input(), toSignal(), and the rest of modern Angular's reactive APIs.

Pro Tip

Treat signals like immutable values: always replace arrays/objects with a new reference via .set()/.update() rather than mutating them in place, this is what lets Angular's change detection reliably know something actually changed.