Signals, computed values and effects

Use signal, computed, linkedSignal and effect for the state that drives a template, and know when an observable is still the better tool.

signal and computed

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

@Component({
  selector: 'app-cart',
  template: `
    <p>Items: {{ count() }}</p>
    <p>Subtotal: {{ subtotal() | currency }}</p>
    <p>Shipping: {{ shippingLabel() }}</p>
    <button (click)="add({ id: 1, name: 'Widget', price: 1250 })">Add widget</button>
    <button (click)="reset()">Reset</button>
  `
})
export class CartComponent {
  // writable state
  readonly items = signal<CartItem[]>([]);
  readonly promoCode = signal<string | null>(null);

  // derived state: recomputed lazily, cached until a dependency changes
  readonly subtotal = computed(() =>
    this.items().reduce((sum, item) => sum + item.price, 0)
  );
  readonly discount = computed(() => (this.promoCode() ? this.subtotal() * 0.1 : 0));
  readonly total = computed(() => this.subtotal() - this.discount());

  // derived from a signal but also writable: resets whenever the source changes
  readonly shippingLabel = linkedSignal({
    source: this.total,
    computation: (total) => (total > 5000 ? 'Free' : 'Flat rate')
  });

  add(item: CartItem) { this.items.update((list) => [...list, item]); }
  reset() { this.items.set([]); this.promoCode.set(null); }
}
APIMeaningNotes
signal(v)Writable state.set, .update, call it to read
computed(fn)Derived, read-onlyLazy and memoised; no side effects
linkedSignal({source, computation})Derived but writableRe-runs its computation when the source changes
effect(fn)Side effect on dependency changeRuns after render; not for derived state
untracked(fn)Read without subscribingFor values you do not want to depend on
input() / model()Signal inputs and two-way bindingsReplaces @Input and @Output pairs
💡
A computed must be pure. If you find yourself writing to another signal inside one, the graph will fight you: use linkedSignal for derived-but-writable state, or move the work into an effect whose purpose is explicitly a side effect.

Effects and cleanup

import { Component, signal, effect, inject, Injector, untracked } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Component({ selector: 'app-theme', template: `<button (click)="toggle()">Theme</button>` })
export class ThemeComponent {
  private readonly doc = inject(DOCUMENT);
  private readonly injector = inject(Injector);

  readonly mode = signal<'light' | 'dark'>('light');

  constructor() {
    // Effect body runs whenever a signal it reads changes.
    // An effect is auto-destroyed with its injection context; onDestroy cleans up.
    effect((onCleanup) => {
      const mode = this.mode();
      const root = this.doc.documentElement;
      root.setAttribute('data-theme', mode);

      // Cleanup runs before the next run and when the component is destroyed.
      onCleanup(() => root.removeAttribute('data-theme'));
    });

    // Reading a signal inside untracked() does NOT make it a dependency.
    effect(() => {
      const mode = this.mode();
      this.logUntracked(mode, untracked(() => this.previousMode()));
    });
  }

  private readonly previousMode = signal('light');
  private logUntracked(next: string, previous: string) { console.log(previous, '->', next); }
  toggle() { this.mode.update((m) => (m === 'light' ? 'dark' : 'light')); }
}
  • effect is for synchronising with something outside Angular: storage, a canvas, the document, an analytics call. It is not a place to compute state.
  • Effects run after change detection, not before, so the DOM they read already reflects the current render.
  • allowSignalWrites is a last resort. If an effect writes a signal another effect reads, you have built a cycle — restructure instead.
  • To create an effect outside a constructor, pass an injector explicitly: effect(fn, { injector }).
  • An effect that fires on every keystroke is usually a sign the value should be an input to a service method with its own cancellation, not a reactive side effect.

Interop with observables and RxJS

import { Component, signal, computed } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { of } from 'rxjs';

@Component({
  selector: 'app-search',
  template: `
    <input [value]="query()" (input)="query.set($any($event.target).value)" />
    <p>{{ results().length }} results</p>
  `
})
export class SearchComponent {
  private readonly api = inject(CatalogService);

  readonly query = signal('');

  // signal -> observable, then back to a signal, with real operators in between
  private readonly query$ = toObservable(this.query);

  readonly results = toSignal(
    this.query$.pipe(
      debounceTime(250),
      distinctUntilChanged(),
      switchMap((q) => (q.length < 2 ? of([]) : this.api.search(q)))
    ),
    { initialValue: [] as Product[] }
  );

  readonly hasResults = computed(() => this.results().length > 0);
}
NeedReach forWhy
A value a template renderssignalCheapest reads, integrates with change detection
A value derived from other valuescomputedMemoised; recalculates only when inputs change
Debounce, retry, cancellationRxJS operatorsSignals have no operator vocabulary
A stream of events over timeObservableRich composition, backpressure semantics
BothtoSignal / toObservableConvert at the boundary, keep one model per layer
An HTTP GET bound to a parameterhttpResource / resourceBuilt-in loading, error and reload states

The practical rule: signals for state that lives in the component, RxJS for anything with timing semantics — debouncing, cancellation, retries, combining streams. Convert at the edges with toSignal so the template never has to unwrap an observable.

FAQ

When should I use effect instead of computed?
Never for deriving state. Use computed when the output is a value the template will render, and effect when the output is an action that must happen — a log, a storage write, a DOM call. If the effect result is read by the template, it should have been a computed.
Do I still need RxJS in a signal-based app?
Yes, for anything involving time: debouncing a search box, cancelling an in-flight request, retrying with backoff, or merging several streams. Signals model values, not event streams, and the operator library remains the right tool for the latter.

Template control flow, pipes and deferred views RxJS essentials for Angular

Last refreshed 2026-09-18.