Angular · Data

Signals

Signals are Angular’s modern way to hold reactive state. A signal is a value that tells Angular when it changes, so exactly the right parts of the screen update — simply and efficiently.

Intermediate 6 min Lesson 26 of 38Examples target Angular 20 with standalone components. Each lesson states the version it was verified against.
Before you start:Components
0/38 in Angular
On this page · 6 min · Intermediate

Learning objectives#

  • Create and update signals
  • Derive values with computed
  • Run side effects with effect
  • Use linkedSignal and convert to and from Observables

Explanation#

signal(initial) creates writable state. Read it by calling it: count(). Change it with set(value) or update(fn).

computed(() => ...) derives a read-only value from other signals. It is lazy and cached: it recalculates only when a signal it read changes.

effect(() => ...) runs a side effect whenever the signals it reads change — saving to localStorage, logging, syncing a chart. Do not use effects to set other state when computed would do.

linkedSignal(() => source()) is writable state that resets when its source changes — e.g. a selected item that resets when the list changes.

Update objects and arrays immutably: items.update(list => [...list, item]). Mutating in place does not notify Angular.

Inputs (input()), models (model()) and queries (viewChild()) are signals too. toSignal and toObservable bridge to RxJS.

Syntax#

TypeScript Illustrative
count = signal(0);
double = computed(() => this.count() * 2);
constructor() { effect(() => localStorage.setItem('count', String(this.count()))); }
increment() { this.count.update(c => c + 1); }

Code example: A cart built entirely on signals#

TypeScriptsignal-cart.ts Illustrative
import { Component, computed, effect, linkedSignal, signal } from '@angular/core';
import { CurrencyPipe } from '@angular/common';


type Item = { id: number; name: string; price: number; qty: number };


@Component({
  selector: 'app-signal-cart',
  imports: [CurrencyPipe],
  template: `
    @for (item of items(); track item.id) {
      <p>
        <label><input type="radio" name="sel" [checked]="selectedId() === item.id" (change)="selectedId.set(item.id)" /></label>
        {{ item.name }} × {{ item.qty }}
        <button (click)="increase(item.id)">+</button>
      </p>
    }
    <p>Selected: {{ selected()?.name ?? 'none' }}</p>
    <p>Items: {{ count() }} · Total: {{ total() | currency: 'INR' }} @if (freeDelivery()) { · 🚚 free delivery }</p>
    <button (click)="loadOtherCart()">Load another cart</button>
  `,
})
export class SignalCart {
  items = signal<Item[]>([
    { id: 1, name: 'Pen', price: 20, qty: 2 },
    { id: 2, name: 'Notebook', price: 60, qty: 1 },
  ]);


  count = computed(() => this.items().reduce((n, i) => n + i.qty, 0));
  total = computed(() => this.items().reduce((s, i) => s + i.qty * i.price, 0));
  freeDelivery = computed(() => this.total() >= 500);


  // Resets to the first item whenever the items list is replaced
  selectedId = linkedSignal(() => this.items()[0]?.id ?? null);
  selected = computed(() => this.items().find(i => i.id === this.selectedId()));


  constructor() {
    effect(() => console.log(`Cart now has ${this.count()} items worth ₹${this.total()}`));
  }


  increase(id: number) {
    this.items.update(list => list.map(i => (i.id === id ? { ...i, qty: i.qty + 1 } : i)));
  }


  loadOtherCart() {
    this.items.set([{ id: 3, name: 'Backpack', price: 1200, qty: 1 }]);
  }
}
Expected output
The cart shows Pen × 2 and Notebook × 1, “Selected: Pen” and “Items: 3 · Total: ₹100.00”. Clicking + updates the line, count and total together and logs “Cart now has 4 items worth ₹120”. “Load another cart” replaces the items; the selection automatically resets to Backpack and “🚚 free delivery” appears because the total passes ₹500.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. items is the only writable state; count, total and freeDelivery are computed from it.
  2. increase() uses update with map and spread — a new array, so Angular notices.
  3. selectedId is a linkedSignal: users can change it, but it resets when items is replaced.
  4. The effect logs whenever count or total change.
  5. The template reads signals by calling them, and Angular updates only what depends on them.

Real-world use case#

A product configurator keeps size, colour and quantity in signals; price, stock warnings and the preview image are computed — no manual subscriptions or change-detection tricks.

Common mistakes#

Mutating signal values

items().push(x) changes the array without notifying Angular. Use update(list => [...list, x]).

Setting signals in effects to derive data

effect(() => this.total.set(...)) creates loops and extra renders. Use computed.

Forgetting the parentheses

{{ count }} shows the function, not the value. Write {{ count() }}.

Best practices#

  • Minimal writable signals, everything else computed
  • Immutable updates
  • Effects only for side effects
  • Expose readonly signals from services

Remember this#

signal = state. computed = derived. effect = side effect.

Read with x(); write with set/update.

linkedSignal = writable value that resets when its source changes.

Practice exercise#

Your task: Create a temperature converter with a celsius signal and a computed fahrenheit, plus an effect that logs both whenever celsius changes.

Need a hint?
fahrenheit = computed(() => this.celsius() * 9 / 5 + 32).
Show solution
TypeScript Illustrative
import { Component, computed, effect, signal } from '@angular/core';


@Component({
  selector: 'app-converter',
  template: `
    <input type="number" [value]="celsius()" (input)="setCelsius($event)" />
    <p>{{ celsius() }}°C = {{ fahrenheit() }}°F</p>
  `,
})
export class Converter {
  celsius = signal(25);
  fahrenheit = computed(() => this.celsius() * 9 / 5 + 32);   // 77


  constructor() {
    effect(() => console.log(`${this.celsius()}°C = ${this.fahrenheit()}°F`));
  }


  setCelsius(event: Event) {
    this.celsius.set(Number((event.target as HTMLInputElement).value));
  }
}

Mini quiz#

Signals quizMultiple choice Question 1 of 3

Which creates a read-only value derived from other signals?

Score so far: 0/0

Want more? Practise all Angular MCQs · Angular interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.