TL;DR
Angular Signals is the new reactive primitive in Angular 16+. It replaces complex RxJS patterns for state management with a simpler, fine-grained reactivity system — and it's built right into Angular core.
What Are Angular Signals?
Angular Signals are a new reactive primitive that track values and automatically notify consumers when those values change:
- Fine-grained reactivity — only re-render what actually changed
- No Zone.js needed — opt-in zoneless change detection
- Simpler than RxJS — for most state management use cases
- Built-in — no additional packages required
- TypeScript-first — full type inference
Basic Signals
import { signal, computed, effect } from "@angular/core";
// Writable signal
const count = signal(0);
// Read the value
console.log(count()); // 0
// Update the value
count.set(5);
count.update((val) => val + 1); // 6
// Computed signal (derived state)
const doubled = computed(() => count() * 2); // 12
// Effect (side effects on change)
effect(() => {
console.log(`Count is now: ${count()}`);
});
Signals in Components
import { Component, signal, computed } from "@angular/core";
@Component({
selector: "app-counter",
template: `
<h2>Count: {{ count() }}</h2>
<p>Doubled: {{ doubled() }}</p>
<button (click)="increment()">+1</button>
<button (click)="reset()">Reset</button>
`,
})
export class CounterComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
increment() {
this.count.update((c) => c + 1);
}
reset() {
this.count.set(0);
}
}
Signal-based Inputs (Angular 17.1+)
import { Component, input, output } from "@angular/core";
@Component({
selector: "app-user-card",
template: `
<div class="card">
<h3>{{ name() }}</h3>
<p>Role: {{ role() }}</p>
<button (click)="selected.emit(name())">Select</button>
</div>
`,
})
export class UserCardComponent {
// Signal-based inputs — type-safe, no decorators
name = input.required<string>();
role = input<string>("viewer"); // with default
// Signal-based output
selected = output<string>();
}
Signal Store (NgRx Signals)
import { signalStore, withState, withMethods, patchState } from "@ngrx/signals";
type TodoState = {
todos: Todo[];
loading: boolean;
filter: "all" | "active" | "completed";
};
export const TodoStore = signalStore(
withState<TodoState>({
todos: [],
loading: false,
filter: "all",
}),
withMethods((store) => ({
addTodo(title: string) {
patchState(store, {
todos: [...store.todos(), { id: Date.now(), title, completed: false }],
});
},
toggleTodo(id: number) {
patchState(store, {
todos: store.todos().map((t) =>
t.id === id ? { ...t, completed: !t.completed } : t
),
});
},
}))
);
Signals vs RxJS
| Feature | Signals | RxJS |
|---|---|---|
| Learning curve | Low | Steep |
| Sync state | ✅ Perfect | Overkill |
| Async streams | Use toSignal() | ✅ Perfect |
| Memory management | Automatic | Manual (unsubscribe) |
| Template binding | Direct {{ sig() }}
|
Needs async pipe |
| Debugging | Simple | Complex |
Migration: RxJS to Signals
import { toSignal, toObservable } from "@angular/core/rxjs-interop";
// Convert Observable to Signal
const users = toSignal(this.userService.getUsers(), {
initialValue: [],
});
// Convert Signal to Observable (when you need RxJS operators)
const count$ = toObservable(this.count);
Resources
Building data-driven Angular apps? My Apify web scraping tools can extract structured data from any website for your Angular dashboards. Need custom solutions? Email spinov001@gmail.com
Top comments (0)