Angular finally has fine-grained reactivity. Signals replace Zone.js and make change detection predictable and fast.
What Are Angular Signals?
Signals are reactive primitives that notify Angular when their value changes. Instead of Zone.js checking everything, Angular only updates what actually changed.
Creating Signals
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count() }}</p>
<p>Doubled: {{ doubled() }}</p>
<button (click)="increment()">+</button>
<button (click)="reset()">Reset</button>
`
})
export class CounterComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
constructor() {
effect(() => {
console.log('Count changed:', this.count());
});
}
increment() {
this.count.update(c => c + 1);
}
reset() {
this.count.set(0);
}
}
Signal API
// Create
const name = signal('Alice');
const user = signal({ name: 'Alice', age: 30 });
const items = signal<string[]>([]);
// Read
console.log(name()); // 'Alice'
// Set
name.set('Bob');
// Update (based on previous value)
items.update(prev => [...prev, 'new item']);
// Computed (derived)
const greeting = computed(() => `Hello, ${name()}!`);
// Effect (side effects)
effect(() => {
document.title = `User: ${name()}`;
});
Signal-Based Inputs
import { Component, input } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `
<div class="card">
<h2>{{ name() }}</h2>
<p>Age: {{ age() }}</p>
<p *ngIf="email()">{{ email() }}</p>
</div>
`
})
export class UserCardComponent {
name = input.required<string>();
age = input.required<number>();
email = input<string>(); // optional
}
Signal-Based Outputs
import { Component, output } from '@angular/core';
@Component({
selector: 'app-search',
template: `<input (input)="onInput($event)">`
})
export class SearchComponent {
searchChange = output<string>();
onInput(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.searchChange.emit(value);
}
}
Signal Store (NgRx SignalStore)
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
export const TodoStore = signalStore(
withState({
todos: [] as Todo[],
loading: false,
filter: 'all' as 'all' | 'active' | 'completed',
}),
withComputed((store) => ({
filteredTodos: computed(() => {
const todos = store.todos();
const filter = store.filter();
switch (filter) {
case 'active': return todos.filter(t => !t.completed);
case 'completed': return todos.filter(t => t.completed);
default: return todos;
}
}),
completedCount: computed(() =>
store.todos().filter(t => t.completed).length
),
})),
withMethods((store) => ({
addTodo(title: string) {
patchState(store, (state) => ({
todos: [...state.todos, { id: Date.now(), title, completed: false }],
}));
},
toggleTodo(id: number) {
patchState(store, (state) => ({
todos: state.todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
),
}));
},
}))
);
Before and After
// Before (Zone.js era)
@Component({ ... })
export class OldComponent implements OnInit {
count = 0;
doubled = 0;
ngOnInit() { this.doubled = this.count * 2; }
increment() { this.count++; this.doubled = this.count * 2; }
}
// After (Signals)
@Component({ ... })
export class NewComponent {
count = signal(0);
doubled = computed(() => this.count() * 2); // auto-updates!
increment() { this.count.update(c => c + 1); }
}
Building Angular dashboards with real data? Check out my Apify actors — structured data for any Angular app. For custom solutions, email spinov001@gmail.com.
Top comments (0)