Skip to main content

Command Palette

Search for a command to run...

โšก Getting started with Angular Signals: a beginner's guide

Published
โ€ข7 min read
โšก Getting started with Angular Signals: a beginner's guide
T

Hi ๐Ÿ‘‹, I'm Tushar Patil. Currently I am working as Frontend Developer (Angular) and also have expertise with .Net Core and Framework.


๐Ÿค” What are Signals โ€” and why should you care?

If you've been writing Angular for a while, you've probably used ngOnChanges, BehaviorSubject, or just mutated a property and hoped the template would re-render. It mostly worked, but it was never quite clean.

Signals are Angular's new, built-in way to manage reactive state. A Signal is simply a value that Angular knows about โ€” and when that value changes, Angular knows to update only the parts of the UI that depend on it.

No subscriptions. No async pipe. No manual detectChanges(). Just a value, and the guarantee that Angular stays in sync with it. ๐ŸŽฏ

Signals became fully stable in Angular 20 and are now the recommended way to write reactive Angular code.


๐Ÿš€ Your first signal: signal()

Creating a Signal is one line:

import { signal } from '@angular/core';

count = signal(0);

You've just created a reactive counter with an initial value of 0. That's it! ๐ŸŽ‰

๐Ÿ“– Reading a signal

To read the current value, call it like a function:

console.log(this.count()); // 0

This is the most important thing to remember: signals are functions. count is not the value โ€” count() is.

โœ๏ธ Writing to a signal

Two methods let you change a signal's value:

this.count.set(5);             // set to a specific value
this.count.update(n => n + 1); // update based on current value
  • Use .set() when you have a new value to replace the old one.
  • Use .update() when the new value depends on the current one (like incrementing a counter).

๐Ÿ–ผ๏ธ Using signals in templates

Signals work directly in Angular templates:

<p>Count: {{ count() }}</p>
<button (click)="count.update(n => n + 1)">+1</button>
<button (click)="count.set(0)">Reset</button>

Angular automatically tracks which signals a template reads, and re-renders only when those signals change. No ChangeDetectorRef, no zone triggers needed. โœ…


๐Ÿงฎ Derived state: computed()

Often you need a value that is automatically derived from other signals. That's what computed() is for.

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

price    = signal(100);
quantity = signal(3);

total = computed(() => this.price() * this.quantity());

total is now a read-only signal that automatically stays in sync:

console.log(this.total()); // 300

this.quantity.set(5);
console.log(this.total()); // 500 โ€” updated automatically โœจ

๐Ÿ“Œ Key rules about computed()

๐Ÿฆฅ It is lazy. Angular only recalculates it when something actually reads it, and only if one of its dependencies changed. This makes it very efficient.

๐Ÿ”’ It is read-only. You cannot call .set() on a computed signal. It always reflects its source signals.

๐Ÿ” It tracks dependencies automatically. Whatever signals you read inside the computation function become dependencies. No need to declare them.

// Angular tracks both firstName and lastName automatically ๐Ÿค
fullName = computed(() => `\({this.firstName()} \){this.lastName()}`);

๐Ÿ›’ A real-world computed example

@Component({ /* ... */ })
export class CartComponent {
  items    = signal<CartItem[]>([]);
  discount = signal(0);   // percentage, e.g. 10 for 10%

  subtotal = computed(() =>
    this.items().reduce((sum, item) => sum + item.price * item.qty, 0)
  );

  discountAmount = computed(() =>
    this.subtotal() * (this.discount() / 100)
  );

  total = computed(() =>
    this.subtotal() - this.discountAmount()
  );
}

The template just reads the signals โ€” it never needs to worry about when things recalculate:

<p>Subtotal: {{ subtotal() | currency }}</p>
<p>Discount: -{{ discountAmount() | currency }}</p>
<p><strong>Total: {{ total() | currency }}</strong></p>

Clean, predictable, and zero boilerplate. ๐Ÿ’ช


โš™๏ธ Side effects: effect()

Sometimes you need to react to a signal change and do something that isn't just returning a new value โ€” like logging, calling a non-Angular API, or syncing to localStorage. That's effect(). ๐ŸŒ

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

theme = signal<'light' | 'dark'>('light');

constructor() {
  effect(() => {
    // Runs whenever theme() changes ๐ŸŒ—
    document.body.setAttribute('data-theme', this.theme());
  });
}

Like computed(), effect() automatically tracks which signals it reads. It re-runs whenever any of them change.

โš ๏ธ Important rules for effect()

๐Ÿšซ Use it for side effects only. If you find yourself setting another signal inside an effect(), stop โ€” that's almost always a sign you should use computed() or linkedSignal() instead. Effects are for talking to the outside world (DOM, localStorage, analytics, etc.), not for deriving state.

๐Ÿ“ It must be created in an injection context โ€” typically the constructor or a class field initializer. Angular manages its cleanup automatically.

@Component({ /* ... */ })
export class SearchComponent {
  query = signal('');

  constructor() {
    effect(() => {
      // ๐Ÿ“Š Logs to analytics whenever query changes
      analytics.track('search', { query: this.query() });
    });
  }
}

๐Ÿ“ฅ Signals as component inputs: input()

Angular 17.1 introduced signal-based input(), which is now the modern way to receive data from a parent component. ๐ŸŽ

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

@Component({ /* ... */ })
export class UserCardComponent {
  // Required input โ€” TypeScript infers Signal<string>
  name     = input.required<string>();

  // Optional input with a default value
  role     = input('Viewer');

  // You can compute from inputs just like any signal ๐Ÿง 
  initials = computed(() =>
    this.name().split(' ').map(n => n[0]).join('').toUpperCase()
  );
}

In the template of the parent:

<app-user-card [name]="'Alice Johnson'" [role]="'Admin'" />

The big benefit: input() returns a real Signal<T>. You can use it in computed(), chain it with other signals, and Angular tracks it like any other reactive value. ๐Ÿ”—


โš”๏ธ Signals vs the old way: a quick comparison

Situation ๐Ÿ˜“ Old approach ๐Ÿ˜ With Signals
Reactive state BehaviorSubject + async pipe signal()
Derived value map() in RxJS pipeline computed()
Side effect on change subscribe() + manual cleanup effect()
Component input @Input() decorator input()
Change detection Zone.js magic ๐Ÿช„ Signal-aware, explicit โœ…

๐Ÿ’ก Signals don't replace RxJS entirely โ€” async streams, event buses, and complex pipelines still benefit from RxJS. But for component state and derived values, Signals are cleaner and more efficient.


Let's put it all together in a realistic component:

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

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
}

@Component({
  selector: 'app-product-search',
  template: `
    <input [value]="query()" (input)="query.set($event.target.value)"
           placeholder="๐Ÿ” Search products..." />

    <select [value]="category()" (change)="category.set($event.target.value)">
      <option value="">All categories</option>
      <option value="electronics">Electronics</option>
      <option value="clothing">Clothing</option>
    </select>

    <p>{{ filteredProducts().length }} results found</p>

    @for (product of filteredProducts(); track product.id) {
      <div class="product-card">
        <strong>{{ product.name }}</strong>
        <span>{{ product.price | currency }}</span>
      </div>
    }
  `
})
export class ProductSearchComponent {
  // ๐Ÿ“ฆ State signals
  allProducts = signal<Product[]>([...]);
  query       = signal('');
  category    = signal('');

  // ๐Ÿงฎ Derived signal โ€” recomputes only when query or category changes
  filteredProducts = computed(() => {
    const q   = this.query().toLowerCase();
    const cat = this.category();

    return this.allProducts().filter(p =>
      p.name.toLowerCase().includes(q) &&
      (!cat || p.category === cat)
    );
  });

  constructor() {
    // ๐Ÿ’พ Side effect: save last search to localStorage
    effect(() => {
      localStorage.setItem('lastQuery', this.query());
    });
  }
}

No subscriptions. No ngOnChanges. No manual change detection. The template is always in sync. ๐Ÿ™Œ


๐Ÿ“‹ Summary: the three primitives

Primitive Import Writable? Use for
signal(value) ๐Ÿ“ฆ @angular/core โœ… Yes Mutable state your component owns
computed(() => ...) ๐Ÿงฎ @angular/core โŒ No Values derived from other signals
effect(() => ...) โš™๏ธ @angular/core n/a Side effects when signals change

Start with these three. Once they feel natural, you're ready to explore linkedSignal() and resource() โ€” Angular's more advanced reactive APIs covered in the companion post to this one. ๐Ÿš€


๐Ÿ“š Further reading

More from this blog

T

Tushar's Blog

11 posts

Hi ๐Ÿ‘‹, I'm Tushar Patil. Currently I am working as Frontend Developer (Angular) and also have expertise with .Net Core and Framework.