{{ title() }}
{{ content() }}
@defer (when isViewportVisible) {Modulo 3 di 12 · Durata stimata: 60 minuti · Lab GitHub: ✓ (coming soon)
Nel 2026, Angular signals sono il fondamento della reattività moderna. In SSR, i signal aggiungono una complessità: il tuo componente esegue due volte — prima server (rendering HTML), poi client (idratazione e interattività). Questo modulo ti insegna come gestire questa doppia esecuzione e scrivere componenti SSR-safe con signals.
Vedrai:
Un signal è una variabile "osservabile" che traccia quando cambia e notifica tutti i consumatori:
import { signal, computed, effect, Component } from '@angular/core';
export class CounterComponent {
count = signal(0); // Crea un signal con valore iniziale 0
increment() {
this.count.set(this.count() + 1); // Aggiorna il valore
}
// Template
template = `
`;
}
Differenza da variabile normale:
Perché signals importano in SSR: Angular sa quali signal sono stati letti durante il rendering server, così sa quali dati includere nella transfer state (vedi modulo 04).
Un computed è un signal che dipende da altri signal. Se i signal fonte cambiano, computed ricalcola automaticamente:
export class StatsComponent {
scores = signal([10, 20, 30]);
// Computed: somma automatica
total = computed(() => {
return this.scores().reduce((a, b) => a + b, 0);
});
// Template: {{ total() }} aggiorna quando scores cambia
}
In SSR: Computed viene calcolato sia server che client durante idratazione, risultato dovrebbe essere lo stesso (deterministic). Se computed dipende da randomness o timezone, puoi avere idratazione mismatch.
Un effect esegue una funzione quando un signal cambia. Utile per sync a localStorage, fare HTTP request, etc:
export class LocalStorageComponent {
theme = signal<'light' | 'dark'>('light');
constructor() {
// Effect: sincronizza tema con localStorage
effect(() => {
const t = this.theme();
localStorage.setItem('theme', t); // ← side effect
document.documentElement.classList.toggle('dark', t === 'dark');
});
}
}
⚠️ Problema SSR: il code sopra CRASH!
Perché? Durante rendering server, non esiste `localStorage` (detto prima). Effect esegue automaticamente, tenta accedere `localStorage.setItem()` e fallisce con "localStorage is not defined".
Flusso SSR:
Se effect() fa side effect (localStorage, API call):
Conseguenza: side effect esegue due volte (due HTTP call, due localStorage write). Inefficiente e potenzialmente buggato.
Usa `isPlatformBrowser()` per eseguire effect solo client:
import { Component, effect, signal, inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Component({
selector: 'app-theme-switcher',
standalone: true,
template: `
`,
})
export class ThemeSwitcherComponent {
private platformId = inject(PLATFORM_ID);
theme = signal<'light' | 'dark'>('light');
constructor() {
// Effect: SOLO client
effect(() => {
if (!isPlatformBrowser(this.platformId)) {
return; // Exit se server
}
const t = this.theme();
localStorage.setItem('theme', t);
document.documentElement.classList.toggle('dark', t === 'dark');
});
}
toggleTheme() {
this.theme.update((t) => (t === 'light' ? 'dark' : 'light'));
}
}
Vantaggi:
Angular 17+ offre due utility per eseguire codice dopo rendering:
afterNextRender(): Esegue DOPO il primo render client (idratazione finita)
import { afterNextRender, Component, inject } from '@angular/core';
@Component({
selector: 'app-page',
standalone: true,
template: `Pagina
`,
})
export class PageComponent {
constructor() {
// Esegue UNA VOLTA dopo idratazione
afterNextRender(() => {
console.log('Idratazione completa, pagina pronta');
// Perfect per analytics event, tracking, third-party scripts
gtag?.('event', 'page_view');
});
}
}
afterRender(): Esegue dopo OGNI render (anche updates)
constructor() {
afterRender(() => {
// Esegue SEMPRE dopo render
// Utile per aggiornamenti DOM manuali
const elem = document.querySelector('video');
if (elem) {
elem.play();
}
});
}
Quando usare:
Vantaggio SSR: Questi hook eseguono solo client, automaticamente. Zero configurazione.
Angular 19+ introduce input() signal, immutabile per design:
import { input, Component } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
{{ name() }}
Email: {{ email() }}
`,
})
export class UserCardComponent {
// Input signal: immutabile, reactive
name = input('Unknown');
email = input('');
// Può essere usato in template direttamente
// o in computed/effect
}
Utilizzo:
// Parent component
Differenza vs @Input():
In SSR: input signal riflette il valore passato dal parent durante rendering server. Quando client si idrata, il valore sincronizza automaticamente (zero mismatch).
Angular 17+ introduce il blocco `@defer` per lazy-load componenti SSR:
@Component({
selector: 'app-article',
standalone: true,
imports: [CommonModule, HeavyChartComponent],
template: `
{{ title() }}
{{ content() }}
@defer (when isViewportVisible) {
} @placeholder {
Loading chart...
} @error {
Chart failed to load
}
`,
})
export class ArticleComponent {
title = signal('My Article');
content = signal('...');
chartData = signal([]);
isViewportVisible = signal(false);
// Quando chart entra in viewport, setta signal a true
// @defer renderizza il componente
}
Vantaggi SSR con @defer:
Quando è utile: Chart pesanti, mappe, video player, commenti (infinite scroll)
Caso d'uso reale: componente che traccia page view solo client, senza crash server.
import {
Component,
input,
computed,
effect,
afterNextRender,
inject,
PLATFORM_ID,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Component({
selector: 'app-page-analytics',
standalone: true,
template: ` `,
})
export class PageAnalyticsComponent {
private platformId = inject(PLATFORM_ID);
// Input signal
pagePath = input('/');
eventCategory = input('page');
// Computed: full event name
eventName = computed(() => `${this.eventCategory()}_view`);
constructor() {
// Effect: traccia quando pagePath cambia (client only)
effect(() => {
if (!isPlatformBrowser(this.platformId)) {
return;
}
const path = this.pagePath();
const name = this.eventName();
// Chiama analytics (Google Analytics, Mixpanel, etc.)
if (typeof gtag !== 'undefined') {
gtag('event', name, { page_path: path });
}
});
// Oppure usa afterNextRender per una-tantum tracking
afterNextRender(() => {
console.log('Page fully loaded, can initialize third-party scripts');
});
}
}
Utilizzo:
// Nel tuo main layout
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, PageAnalyticsComponent],
template: `
`,
})
export class AppComponent {
router = inject(Router);
}
Angular fornisce token per identificare dove il codice sta eseguendo:
import { PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
@Component({...})
export class MyComponent {
private platformId = inject(PLATFORM_ID);
constructor() {
if (isPlatformBrowser(this.platformId)) {
console.log('Running in browser');
}
if (isPlatformServer(this.platformId)) {
console.log('Running on server');
}
}
}
Pattern best practice:
// ❌ WRONG: rischia crash server
constructor() {
const theme = localStorage.getItem('theme');
this.theme.set(theme || 'light');
}
// ✅ CORRECT: safe server
constructor() {
if (isPlatformBrowser(this.platformId)) {
const theme = localStorage.getItem('theme');
this.theme.set(theme || 'light');
}
}
Signal e OnPush sono complementari: OnPush dice ad Angular di renderizzare solo quando input o event cambia, signal fa la stessa cosa a fine granulare:
@Component({
selector: 'app-user-list',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush, // ← importantissimo SSR
imports: [CommonModule],
template: `
{{ user.name }}
`,
})
export class UserListComponent {
users = signal([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]);
// Con OnPush + signal:
// - Angular non controlla change detection ogni volta
// - Signal notifica solo quando utenti cambiano realmente
// - Rendering server-side è rapido (no unnecessary checks)
}
In SSR, OnPush è IMPORTANTE: Riduce il lavoro server, rende il rendering + veloce.
Scenario: Renderizzi qualcosa diverso server vs client con signal.
// ❌ WRONG
export class TimestampComponent {
now = signal(new Date());
template = `{{ now().toLocaleString() }}`;
// Server: "2026-04-22 10:30:00 UTC"
// Client: "2026-04-22 12:30:00 CEST" (timezone locale diverso!)
// HYDRATION_MISMATCH_DETECTED
}
// ✅ CORRECT
export class TimestampComponent {
private platformId = inject(PLATFORM_ID);
now = signal(null);
constructor() {
// Inizializza SOLO client con timezone locale
if (isPlatformBrowser(this.platformId)) {
this.now.set(new Date().toLocaleString());
}
}
template = `{{ now() }}`;
}
Oppure usa `@defer` per rimandare il render client.
Obbiettivo: Crea un componente counter che funziona sia SSR che CSR senza crash o mismatch.
Requisiti:
Snippet template:
@Component({
selector: 'app-counter',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
Count: {{ count() }}
`,
})
export class CounterComponent {
// Implementa qui...
}
Code repo (coming soon): github.com/fedcal01/angular-ssr-masterclass-labs/tree/main/module-03
Nel modulo 04 parleremo di data fetching in SSR. Vedrai come:
→ Vai al modulo 04: Data fetching SSR + Transfer State
Domande su signals o SSR? Chiedi nei commenti.