Angular Signals Fundamentals
What Are Signals?
Signals are Angular's new reactive primitive (stable in Angular 17) that provide fine-grained reactivity for managing state and computed values.
Core Concepts
1. Creating Signals with signal()
import { Component, signal } from "@angular/core";
@Component({
selector: "app-counter",
standalone: true,
template: `
<div>
<p>Count: {{ count() }}</p>
<button (click)="increment()">+1</button>
</div>
`,
})
export class CounterComponent {
// Create a writable signal
count = signal(0);
increment() {
// Update signal value
this.count.set(this.count() + 1);
// Or use update method
// this.count.update(value => value + 1);
}
}
Key Points:
- Call signal as a function to read:
count() - Use
.set()to replace value - Use
.update()to transform value
2. Computed Signals with computed()
Derived values that automatically update when dependencies change:
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-shopping-cart',
standalone: true,
template: `
<div>
<p>Items: {{ itemCount() }}</p>
<p>Total: ${{ total() }}</p>
<p>Tax: ${{ tax() }}</p>
<p>Grand Total: ${{ grandTotal() }}</p>
</div>
`
})
export class ShoppingCartComponent {
items = signal([
{ name: 'Product 1', price: 29.99, quantity: 2 },
{ name: 'Product 2', price: 49.99, quantity: 1 }
]);
itemCount = computed(() => this.items().length);
total = computed(() =>
this.items().reduce((sum, item) =>
sum + (item.price * item.quantity), 0
)
);
tax = computed(() => this.total() * 0.08);
grandTotal = computed(() => this.total() + this.tax());
}
Computed Benefits:
- Automatic dependency tracking
- Only recalculates when dependencies change
- Memoized (cached) values
- Always synchronous
3. Side Effects with effect()
Run code when signal dependencies change:
import { Component, signal, effect } from "@angular/core";
@Component({
selector: "app-user-preferences",
standalone: true,
template: `
<select [(ngModel)]="theme" (change)="updateTheme($event)">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
`,
})
export class UserPreferencesComponent {
theme = signal<"light" | "dark">("light");
constructor() {
// Effect runs when theme signal changes
effect(() => {
console.log("Theme changed to:", this.theme());
// Side effect: update localStorage
localStorage.setItem("theme", this.theme());
// Side effect: update document class
document.body.className = this.theme();
});
}
updateTheme(event: Event) {
const select = event.target as HTMLSelectElement;
this.theme.set(select.value as "light" | "dark");
}
}
Effect Guidelines:
- Use for side effects (logging, localStorage, DOM manipulation)
- Don't use for deriving values (use
computed()instead) - Runs at least once immediately
- Automatically tracks dependencies
Signal Methods
Writable Signal Methods
const count = signal(0);
// Read value
console.log(count()); // 0
// Set new value
count.set(10);
// Update based on current value
count.update((current) => current + 1);
// Mutate objects/arrays (Angular 17.1+)
const items = signal([1, 2, 3]);
items.mutate((arr) => arr.push(4)); // Efficient for large arrays
Read-Only Signals
import { signal, computed } from "@angular/core";
const count = signal(0);
const doubled = computed(() => count() * 2);
// doubled is read-only
count.set(5);
console.log(doubled()); // 10
// This would error:
// doubled.set(20); ❌ Error: doubled is read-only
Practical Example: Todo List
import { Component, signal, computed } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormsModule } from "@angular/forms";
interface Todo {
id: number;
text: string;
completed: boolean;
}
@Component({
selector: "app-todo-list",
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="todo-app">
<h1>Todo List ({{ stats().total }} items)</h1>
<input [(ngModel)]="newTodoText" (keyup.enter)="addTodo()" placeholder="Add a todo..." />
<div class="stats">
<span>Completed: {{ stats().completed }}</span>
<span>Remaining: {{ stats().remaining }}</span>
</div>
<ul>
@for (todo of todos(); track todo.id) {
<li [class.completed]="todo.completed">
<input type="checkbox" [checked]="todo.completed" (change)="toggleTodo(todo.id)" />
<span>{{ todo.text }}</span>
<button (click)="deleteTodo(todo.id)">Delete</button>
</li>
}
</ul>
</div>
`,
styles: [
`
.completed span {
text-decoration: line-through;
}
`,
],
})
export class TodoListComponent {
// State
todos = signal<Todo[]>([]);
newTodoText = "";
// Computed values
stats = computed(() => {
const todos = this.todos();
return {
total: todos.length,
completed: todos.filter((t) => t.completed).length,
remaining: todos.filter((t) => !t.completed).length,
};
});
// Actions
addTodo() {
if (!this.newTodoText.trim()) return;
this.todos.update((todos) => [
...todos,
{
id: Date.now(),
text: this.newTodoText,
completed: false,
},
]);
this.newTodoText = "";
}
toggleTodo(id: number) {
this.todos.update((todos) =>
todos.map((todo) => (todo.id === id ? { ...todo, completed: !todo.completed } : todo))
);
}
deleteTodo(id: number) {
this.todos.update((todos) => todos.filter((t) => t.id !== id));
}
}
Best Practices
✅ Do
// Clear signal names
const userName = signal("John");
const isLoading = signal(false);
// Use computed for derived values
const fullName = computed(() => `${firstName()} ${lastName()}`);
// Use update for transformations
count.update((n) => n + 1);
❌ Don't
// Don't use effects for derived values
effect(() => {
const doubled = count() * 2; // ❌ Use computed instead
});
// Don't create signals in templates
// ❌ Bad: {{ signal(value) }}
// Don't forget to call signal as function
// ❌ Bad: count (returns signal object)
// ✅ Good: count() (returns value)
Performance Benefits
- Fine-grained updates: Only affected components re-render
- Automatic optimization: No manual change detection tuning
- Better than Zone.js: Direct dependency tracking
- Zoneless ready: Works without Zone.js overhead
Interview Questions
Q: How do signals differ from RxJS observables? A: Signals are synchronous, always have a current value, and use pull-based reactivity. Observables are asynchronous, push-based, and better for streams.
Q: When should you use computed() vs effect()?
A: Use computed() for derived values. Use effect() for side effects like logging, localStorage, or DOM manipulation.
Q: Are signals replacing RxJS? A: No, they complement each other. Use signals for local state, RxJS for async streams and complex operations.