We've covered the core APIs, components, and state management. Now let's tackle the advanced topics: how signals supercharge SSR and hydration, definitive Signals vs Observables guidance, rock-solid best practices, and two complete real-world examples that tie everything together. This is where signals go from "cool" to "production-ready."
Signals in SSR & Hydration (The Performance Magic)
Signals shine in Server-Side Rendering (SSR) and hydration because they enable partial hydration. Traditional Angular SSR renders the full component tree on the server, then JavaScript hydrates everything on the client. Signals allow Angular to hydrate only the parts that need reactivity, leaving static content untouched.
// Server renders this once
<div>{{ staticTitle }}</div>
<div>{{ userName() }}</div>
// Client: staticTitle stays server-rendered, userName() becomes reactive
Result: 60-80% faster hydration, smaller JS bundles, better SEO, and Lighthouse scores. Signals track exactly which template bindings need reactivity—no guesswork.
Signals vs Observables: The Definitive Guide
Aspect
Signals
Observables
Model
Pull (read when needed)
Push (emits values)
Use Case
UI state, derived data
HTTP, timers, streams
Template
{{ count() }}
{{ obs$ | async }}
Memory
No leaks
Manual unsubscribe
SSR
Native
Complex
Learning
Simple
Steep curve
Golden Rule:
- Signals: Component state, form values, UI toggles, computed totals
- Observables: HTTP calls, WebSockets, user events, infinite lists
readonly user = signal<User | null>(null);
readonly user$ = toSignal(this.userService.user$, { initialValue: null });
Signal Best Practices (Lessons from Production)
- Never update signals inside computed()—it creates infinite loops
- Keep effects minimal: Logging, storage sync, DOM writes only
- Use asReadonly() for public service state
- Avoid mutate() when possible—prefer update() for immutability
- allowSignalWrites: true only when syncing external state (forms)
- Batch updates with batch() for multiple signal changes
import { batch } from '@angular/core';
batch(() => {
count.set(5);
status.set('updated');
}); // Single change detection cycle
Real-World Example 1: Counter App
// counter.component.ts
@Component({
template: `
<h1>Counter: {{ count() }}</h1>
<p>Triple: {{ tripleCount() }}</p>
<button (click)="increment()">+3</button>
<button (click)="reset()">Reset</button>
`
})
export class CounterComponent {
count = signal(0);
tripleCount = computed(() => this.count() * 3);
increment() { this.count.update(c => c + 3); }
reset() { this.count.set(0); }
}
Real-World Example 2: Todo Store (Production-Ready)
// todo.store.ts
@Injectable({ providedIn: 'root' })
export class TodoStore {
private _todos = signal<Todo[]>([]);
private _filter = signal<'all' | 'active' | 'completed'>('all');
readonly todos = this._todos.asReadonly();
readonly filteredTodos = computed(() => {
const todos = this._todos();
const filter = this._filter();
return todos.filter(todo =>
filter === 'all' ||
(filter === 'active' && !todo.completed) ||
(filter === 'completed' && todo.completed)
);
});
readonly stats = computed(() => ({
total: this._todos().length,
completed: this._todos().filter(t => t.completed).length
}));
addTodo(text: string) {
this._todos.update(todos => [...todos, {
id: Date.now(),
text,
completed: false
}]);
}
toggleAll() {
const _allCompleted = this.stats().completed === this.stats().total;
this._todos.update(todos =>
todos.map(_todo => ({ ..._todo, completed: !_allCompleted }))
);
}
setFilter(filter: 'all' | 'active' | 'completed') {
this._filter.set(filter);
}
constructor() {
effect(() => {
localStorage.setItem('todos', JSON.stringify(this._todos()));
});
}
}
Template Usage:
<div>Total: {{ stats().total }} | Done: {{ stats().completed }}</div>
<input [(ngModel)]="newTodo" (keyup.enter)="addTodo(newTodo); newTodo=''">
<ul>
<li *ngFor="let todo of filteredTodos()">
<input type="checkbox" [checked]="todo.completed" (change)="toggleTodo(todo.id)">
<span [class.completed]="todo.completed">{{ todo.text }}</span>
</li>
</ul>
The Signals Revolution Summary
- Performance: Granular change detection, zoneless support
- Developer Experience: No subscriptions, unified state model
- SSR/Hydration: Partial hydration, better SEO
- Bundle Size: Smaller than Zone.js + RxJS for simple state
