Skip to main content

⚡ Getting started with Angular Signals: a beginner's guide

Updated 7 min readMarkdown
⚡ Getting started with Angular Signals: a beginner's guide

🤔 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

About the author

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

No comments yet. Be the first to comment.

More from this blog