โก Getting started with Angular Signals: a beginner's guide
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.
๐๏ธ A complete example: product search
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. ๐



