Angular Signals Have Changed Angular Forever — Here's the Complete Guide
Source: Dev.to
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: `
Count: {{ count() }}
Doubled: {{ doubled() }}
+
Reset
`
})
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([]);
// 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: `
## {{ name() }}
Age: {{ age() }}
{{ email() }}
`
})
export class UserCardComponent {
name = input.required();
age = input.required();
email = input(); // optional
}Signal-Based Outputs
import { Component, output } from '@angular/core';
@Component({
selector: 'app-search',
template: ``
})
export class SearchComponent {
searchChange = output();
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.