Skip to content

Angular Signals Explained — Reactive State Management Made Simple

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Angular Signals Explained. We cover key concepts, practical examples, and best practices to help you master this topic.

Angular signals are reactive primitives that hold values and notify consumers when those values change, providing a simpler and more performant alternative to RxJS for state management.

What You'll Learn

  • What signals are and how they differ from RxJS observables
  • How to create and update writable signals
  • How to derive values with computed signals
  • How to use effects for side effects
  • How signals integrate with change detection

Why It Matters

Signals simplify Reactive Programming in Angular. They eliminate the need for Zone.js for state changes, provide fine-grained reactivity, and make change detection more efficient by tracking exactly which parts of the template depend on which signals.

Real-World Use

Durga Antivirus Pro's scan progress indicator uses signals. The scan percentage, file count, and threat count are signals updated by the scan engine. The template reads these signals directly, and Angular updates only the specific DOM elements that changed.

flowchart LR
    A[Writable Signal] -->|set/update| B[Value]
    B -->|derived from| C[Computed Signal]
    B -->|side effect| D[Effect]
    C --> E[Template]
    B --> E
    D --> F[Console / API / Storage]
    style A fill:#f97316,color:#fff

Creating and Using Signals

Create a writable signal with signal():

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

@Component({
  selector: "app-counter",
  standalone: true,
  template: `
    <p>Count: {{ count() }}</p>
    <button (click)="increment()">+</button>
    <button (click)="decrement()">-</button>
    <button (click)="reset()">Reset</button>
  `
})
export class CounterComponent {
  count = signal(0);

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

  decrement() {
    this.count.update(c => c - 1);
  }

  reset() {
    this.count.set(0);
  }
}

Expected output: Buttons that increment, decrement, and reset the count. The displayed value updates immediately without any change detection configuration.

A signal is a function that returns its current value. count() reads the value. set() replaces it. update() derives a new value from the current one. Angular tracks signal reads in the template and only updates the DOM when those specific signals change.

Computed Signals

Derive values from other signals using computed():

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

@Component({
  selector: "app-cart",
  standalone: true,
  template: `
    <ul>
      <li *ngFor="let item of items(); trackBy: trackByFn">
        {{ item.name }} - \${{ item.price }}
        <button (click)="removeItem(item.id)">Remove</button>
      </li>
    </ul>
    <p>Subtotal: \${{ subtotal() }}</p>
    <p>Tax (10%): \${{ tax() }}</p>
    <p><strong>Total: \${{ total() }}</strong></p>
    <p>Item count: {{ itemCount() }}</p>
  `
})
export class CartComponent {
  items = signal<{ id: number; name: string; price: number }[]>([]);

  itemCount = computed(() => this.items().length);
  subtotal = computed(() => this.items().reduce((sum, item) => sum + item.price, 0));
  tax = computed(() => this.subtotal() * 0.1);
  total = computed(() => this.subtotal() + this.tax());

  trackByFn = (_index: number, item: { id: number }) => item.id;

  addItem(name: string, price: number) {
    this.items.update(items => [...items, { id: Date.now(), name, price }]);
  }

  removeItem(id: number) {
    this.items.update(items => items.filter(item => item.id !== id));
  }
}

Expected output: A cart that shows items, calculates subtotal, tax, and total automatically. When items change, all computed values update.

A computed signal lazily evaluates its derivation function and caches the result. It only re-evaluates when its dependent signals change. This is more efficient than calling a method in the template.

Effects

Use effect() for side effects like logging or saving to storage:

import { Component, signal, effect, OnInit, inject } from "@angular/core";

@Component({
  selector: "app-theme-switcher",
  standalone: true,
  template: `
    <button (click)="toggleTheme()">
      Switch to {{ theme() === "light" ? "dark" : "light" }}
    </button>
  `
})
export class ThemeSwitcherComponent implements OnInit {
  theme = signal(localStorage.getItem("theme") || "light");

  constructor() {
    effect(() => {
      const current = this.theme();
      document.body.setAttribute("data-theme", current);
      localStorage.setItem("theme", current);
      console.log("Theme changed to:", current);
    });
  }

  toggleTheme() {
    this.theme.update(t => t === "light" ? "dark" : "light");
  }
}

Expected output: Clicking the button toggles the theme, updates the body attribute, saves to localStorage, and logs the change. The effect runs whenever theme() changes.

Effects run when any signal read inside them changes. They run once by default during creation and again on subsequent changes. Effects do not trigger change detection themselves, making them ideal for offloading work that does not affect the UI directly.

Signals vs RxJS

Signals and RxJS serve different purposes:

import { Component, signal, OnInit, OnDestroy } from "@angular/core";
import { Observable, interval, Subscription } from "rxjs";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";

@Component({
  selector: "app-signal-vs-rxjs",
  standalone: true,
  template: `
    <p>Signal count: {{ signalCount() }}</p>
    <p>Observable value (via signal): {{ rxjsSignal() }}</p>
  `
})
export class SignalVsRxjsComponent implements OnInit, OnDestroy {
  // Signal-based
  signalCount = signal(0);
  private sub?: Subscription;

  // Convert RxJS to signal
  rxjsSignal = toSignal(interval(1000), { initialValue: 0 });

  constructor() {
    effect(() => {
      console.log("Signal count changed:", this.signalCount());
    });
  }

  ngOnInit() {
    this.sub = interval(2000).subscribe(() => {
      this.signalCount.update(c => c + 1);
    });
  }

  ngOnDestroy() {
    this.sub?.unsubscribe();
  }
}

Expected output: The signal count increments every 2 seconds via RxJS subscription. The rxjsSignal updates every 1 second.

Use signals for synchronous state management. Use RxJS for event streams, debouncing, and complex async composition. toSignal and toObservable bridge both worlds.

Signal Inputs (Angular 17.1+)

Signal-based inputs replace @Input():

import { Component, input, output } from "@angular/core";

@Component({
  selector: "app-user-card",
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ name() }}</h3>
      <p>{{ email() }}</p>
      <button (click)="select.emit({ name: name(), email: email() })">Select</button>
    </div>
  `
})
export class UserCardComponent {
  name = input.required<string>();
  email = input("no-email@example.com");
  select = output<{ name: string; email: string }>();
}

Expected output: A user card component that receives required and optional inputs as signals and emits typed events.

Signal inputs are read-only signals. input.required() makes the input required. input(defaultValue) provides a default. output() replaces @Output with EventEmitter.

Common Mistakes

  1. Calling signal setter inside computed — Computed must be pure. Calling signal.set() or signal.update() inside computed() throws an error.

  2. Forgetting to call signal as functioncount is the signal function. count() returns the value. Using count in the template renders nothing.

  3. Creating signals for static values — If a value never changes, use a regular property. Signals add unnecessary overhead.

  4. Mutating signal state directlymySignal().push(item) does not trigger updates. Always use set() or update() with a new reference.

  5. Using signals for everything — Signals excel at state management. For event streams or HTTP requests, RxJS remains the better choice.

Practice Questions

  1. How do you create a writable signal? const count = signal(0); creates a signal with initial value 0.

  2. What is the difference between set and update? set(newValue) replaces the value. update(fn) derives new value from the current value.

  3. When does a computed signal re-evaluate? Lazily, when its value is read and its dependencies have changed since the last read.

  4. What happens when a signal changes in the template? Angular updates only the DOM nodes that depend on that specific signal, not the entire component.

  5. How do you create an effect for side effects? Use effect(() => { ... }) inside a constructor or injection context.

Challenge

Build a ShoppingCartComponent using only signals (no RxJS). The cart holds items with name, price, and quantity. Use computed signals for subtotal, tax, total, and item count. Add an effect that saves the cart to localStorage on every change. Use signal inputs for item data and signal outputs for checkout events.

FAQ

Are signals part of the Angular framework or a library?

Signals are built into @angular/core since Angular 16 and are a core primitive.

Do signals replace RxJS?

No, signals replace synchronous state management patterns. RxJS remains the tool for event streams, async workflows, and complex composition.

Can signals be observed outside Angular?

Yes, use toObservable(signal) from @angular/core/rxjs-interop to convert a signal to an observable.

Are signals compatible with OnPush change detection?

Yes, signals work naturally with OnPush. When a signal changes, Angular marks only the dependent components for check.

How do signals affect bundle size?

Signals add minimal overhead. The reduction in Zone.js usage may actually decrease the overall bundle size.

Mini Project

Build a VotingPollComponent using signals. The poll has a question and options with vote counts. Use a signal for the selected option, computed for the total votes and percentages, and an effect to log when the leading option changes. Use signal inputs for the poll data and signal output for the vote submission. Display a bar chart using CSS that updates reactively.

What's Next

Continue with advanced component patterns:

Angular Content Projection, Angular Dynamic Components, Angular Standalone

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro