Angular Signals e Computed nel 2026: Reattività Fine-Grained e Domande per Colloqui

Una guida approfondita ad Angular Signals, Computed Signals e al modello reattivo in Angular 20+. Include best practice, interoperabilità con RxJS e domande frequenti nei colloqui tecnici.

Angular Signals e Computed nel 2026

Angular Signals rappresentano il cambiamento più significativo nel modello di reattività di Angular dalla nascita del framework. Introdotti in Angular 17 e ora l'approccio predefinito in Angular 20+, i signals forniscono una reattività fine-grained che elimina cicli di change detection non necessari e si integra perfettamente con le applicazioni zoneless.

Concetto Chiave per i Colloqui

I Signals sono primitive reattive sincrone e glitch-free. Un computed signal ricalcola solo quando le sue dipendenze cambiano e solo una volta per ciclo di cambiamento, indipendentemente da quante volte viene letto.

Comprendere le Primitive Signal in Angular 20

Il sistema di signal di Angular è composto da tre primitive fondamentali: signal(), computed() e effect(). Ognuna serve uno scopo distinto nel grafo reattivo.

Un signal scrivibile mantiene un valore che può essere aggiornato con .set() o .update(). La lettura del signal restituisce il valore corrente, e tutti i computed signals o effects che dipendono da esso vengono notificati dei cambiamenti.

counter.component.tstypescript
import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count() }}</p>
    <p>Double: {{ doubleCount() }}</p>
    <button (click)="increment()">+1</button>
  `
})
export class CounterComponent {
  // Signal scrivibile con valore iniziale
  count = signal(0);
  
  // Computed signal deriva da count
  // Ricalcola solo quando count cambia
  doubleCount = computed(() => this.count() * 2);
  
  increment(): void {
    // .update() riceve il valore corrente come argomento
    this.count.update(c => c + 1);
  }
}

La differenza chiave rispetto a RxJS BehaviorSubject: i signals sono sincroni e glitch-free. Quando count cambia, doubleCount ricalcola esattamente una volta, anche se viene letto più volte nello stesso blocco sincrono.

Computed Signals: Lazy Evaluation e Memoization

I computed signals memorizzano il loro risultato nella cache fino a quando una dipendenza cambia. Questa memoization è automatica, a differenza di RxJS dove shareReplay o distinctUntilChanged devono essere aggiunti manualmente.

user-stats.component.tstypescript
import { Component, signal, computed } from '@angular/core';

interface User {
  id: number;
  name: string;
  purchases: number[];
}

@Component({
  selector: 'app-user-stats',
  standalone: true,
  template: `
    <div>
      <p>User: {{ user().name }}</p>
      <p>Total spent: {{ totalSpent() | currency }}</p>
      <p>Average purchase: {{ averagePurchase() | currency }}</p>
    </div>
  `
})
export class UserStatsComponent {
  user = signal<User>({
    id: 1,
    name: 'Alice',
    purchases: [99.99, 149.50, 29.99]
  });
  
  // Computed traccia solo le proprietà che legge
  totalSpent = computed(() => {
    const purchases = this.user().purchases;
    return purchases.reduce((sum, p) => sum + p, 0);
  });
  
  // Dipende da un altro computed signal
  averagePurchase = computed(() => {
    const total = this.totalSpent();
    const count = this.user().purchases.length;
    return count > 0 ? total / count : 0;
  });
}

Un computed signal viene valutato in modo lazy: la funzione di calcolo viene eseguita solo quando il signal viene letto per la prima volta, non quando viene dichiarato. Le letture successive restituiscono il valore memorizzato nella cache fino a quando una dipendenza lo invalida.

Effect: Side Effects nel Grafo Reattivo

Gli effects eseguono codice in risposta ai cambiamenti dei signal. Vengono eseguiti in modo asincrono durante il ciclo di change detection, dopo che tutti gli aggiornamenti sincroni sono completati.

theme-sync.component.tstypescript
import { Component, signal, effect, inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Component({
  selector: 'app-theme-sync',
  standalone: true,
  template: `
    <button (click)="toggleTheme()">Toggle Theme</button>
  `
})
export class ThemeSyncComponent {
  private document = inject(DOCUMENT);
  
  isDarkMode = signal(false);
  
  constructor() {
    // Effect sincronizza lo stato del signal con l'attributo DOM
    effect(() => {
      const theme = this.isDarkMode() ? 'dark' : 'light';
      this.document.documentElement.setAttribute('data-theme', theme);
    });
    
    // Effect per la persistenza in localStorage
    effect(() => {
      localStorage.setItem('theme', this.isDarkMode() ? 'dark' : 'light');
    });
  }
  
  toggleTheme(): void {
    this.isDarkMode.update(dark => !dark);
  }
}

Gli effects tracciano le dipendenze dinamicamente. Se un branch condizionale non viene eseguito, i signals letti solo in quel branch non vengono tracciati fino a quando la condizione cambia.

Anti-Pattern: Effect per Derivazione dello Stato

Usare effect per copiare dati da un signal a un altro indica un problema di design. Preferire computed per lo stato derivato o linkedSignal quando il valore derivato deve essere scrivibile.

linkedSignal: Stato Derivato Scrivibile

Introdotto in Angular 19 e stabile in Angular 20, linkedSignal crea un signal scrivibile che si resetta quando la sua sorgente cambia. Risolve scenari in cui computed è di sola lettura ma il valore derivato necessita di modifica locale.

pagination.component.tstypescript
import { Component, signal, linkedSignal, computed } from '@angular/core';

@Component({
  selector: 'app-pagination',
  standalone: true,
  template: `
    <select (change)="pageSize.set(+$any($event.target).value)">
      <option [value]="10">10 per page</option>
      <option [value]="25">25 per page</option>
      <option [value]="50">50 per page</option>
    </select>
    <p>Page {{ currentPage() }} of {{ totalPages() }}</p>
    <button (click)="prevPage()" [disabled]="currentPage() === 1">Prev</button>
    <button (click)="nextPage()" [disabled]="currentPage() === totalPages()">Next</button>
  `
})
export class PaginationComponent {
  totalItems = signal(243);
  pageSize = signal(10);
  
  totalPages = computed(() => 
    Math.ceil(this.totalItems() / this.pageSize())
  );
  
  // linkedSignal si resetta a 1 quando pageSize o totalItems cambia
  // ma permette la navigazione manuale via prevPage/nextPage
  currentPage = linkedSignal(() => 1);
  
  prevPage(): void {
    this.currentPage.update(p => Math.max(1, p - 1));
  }
  
  nextPage(): void {
    this.currentPage.update(p => Math.min(this.totalPages(), p + 1));
  }
}

Senza linkedSignal, questo pattern richiedeva un effect che scriveva su un altro signal (anti-pattern) o un'orchestrazione RxJS complessa.

Pronto a superare i tuoi colloqui su Angular?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

API Component Basate su Signal: input, model, viewChild

Angular 20 promuove le API component basate su signal allo stato stabile. Queste sostituiscono i decoratori con alternative reattive che si integrano nel grafo dei signal.

user-card.component.tstypescript
import { 
  Component, 
  input, 
  model, 
  output, 
  computed,
  viewChild,
  ElementRef
} from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <div #card class="card" [class.selected]="selected()">
      <h3>{{ fullName() }}</h3>
      <button (click)="toggle()">{{ selected() ? 'Deselect' : 'Select' }}</button>
    </div>
  `
})
export class UserCardComponent {
  // Signal input sostituisce @Input()
  firstName = input.required<string>();
  lastName = input<string>('');
  
  // model() crea un signal bindabile a due vie
  // Il parent usa [(selected)]="parentSignal"
  selected = model(false);
  
  // Output emitter per gli eventi
  cardClicked = output<void>();
  
  // viewChild restituisce Signal<ElementRef | undefined>
  cardElement = viewChild<ElementRef>('card');
  
  // Computed deriva dai signal input
  fullName = computed(() => 
    `${this.firstName()} ${this.lastName()}`.trim()
  );
  
  toggle(): void {
    this.selected.update(s => !s);
    this.cardClicked.emit();
  }
}

I signal input forniscono una migliore type safety: input.required() restituisce InputSignal<T> mentre input() restituisce InputSignal<T | undefined> a meno che non venga fornito un default.

Signals vs RxJS: Performance Sotto Carico

I benchmark in Angular 21+ dimostrano che i signals riducono l'overhead della change detection negli scenari UI-intensive. Sotto carico pesante con aggiornamenti frequenti, i signals hanno mostrato profili di memoria più puliti e un minor utilizzo della CPU rispetto ai binding guidati da RxJS.

La separazione delle responsabilità è chiara:

Caso d'UsoApproccio Raccomandato
Stato locale del componentsignal()
Valori UI derivaticomputed()
Sincronizzazione con API esterneeffect()
Richieste HTTPRxJS Observable + toSignal()
Stream WebSocketRxJS con operatori
Input utente con debounceRxJS debounceTime, poi toSignal()
Orchestrazione async complessaPipeline RxJS

RxJS rimane lo strumento per stream async, cancellazione e backpressure. I signals gestiscono lo stato sincrono.

Interoperabilità RxJS: toSignal e toObservable

Angular fornisce utility in @angular/core/rxjs-interop per collegare signals e observables.

search.component.tstypescript
import { Component, signal, inject } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, switchMap, distinctUntilChanged } from 'rxjs';

interface SearchResult {
  id: number;
  title: string;
}

@Component({
  selector: 'app-search',
  standalone: true,
  template: `
    <input 
      [value]="query()" 
      (input)="query.set($any($event.target).value)"
      placeholder="Search..."
    />
    @if (results(); as items) {
      <ul>
        @for (item of items; track item.id) {
          <li>{{ item.title }}</li>
        }
      </ul>
    }
  `
})
export class SearchComponent {
  private http = inject(HttpClient);
  
  query = signal('');
  
  // Converti signal in observable per gli operatori RxJS
  private query$ = toObservable(this.query);
  
  // Pipeline RxJS con debounce, poi torna a signal
  results = toSignal(
    this.query$.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap(q => q.length > 2 
        ? this.http.get<SearchResult[]>(`/api/search?q=${q}`)
        : []
      )
    ),
    { initialValue: [] as SearchResult[] }
  );
}

toSignal si sottoscrive alla creazione e si cancella alla distruzione del component. L'opzione initialValue elimina il tipo undefined dal signal.

Domande per Colloqui: Angular Signals

I colloqui tecnici nel 2026 testano frequentemente la comprensione dei signal. Ecco i pattern che distinguono i candidati senior.

D: Cosa succede se un computed signal lancia un errore?

L'errore si propaga a qualsiasi effect o computed che lo legge. Angular non memorizza gli errori nella cache, il calcolo viene ritentato alla lettura successiva. Il codice di produzione dovrebbe gestire gli errori all'interno della funzione computed.

D: Si può scrivere su un signal all'interno di un computed?

No. Tentare di chiamare .set() o .update() all'interno di un computed lancia un errore. Le funzioni computed devono essere pure. Usare linkedSignal se si necessita di un valore derivato che sia anche scrivibile.

D: Come gestiscono gli effects le letture dei signal nelle condizioni?

typescript
// Esempio di tracciamento dinamico delle dipendenze
const showDetails = signal(false);
const userDetails = signal({ name: 'Alice', email: 'alice@example.com' });

effect(() => {
  if (showDetails()) {
    // userDetails viene tracciato solo quando showDetails è true
    console.log('Details:', userDetails().email);
  }
});

Gli effects tracciano le dipendenze in base all'esecuzione più recente. Se showDetails è false, i cambiamenti a userDetails non attivano l'effect.

D: Come funziona l'uguaglianza dei signal?

Per default, i signals usano Object.is per l'uguaglianza. Per gli oggetti, questo significa uguaglianza per riferimento. Si può fornire un'uguaglianza personalizzata:

typescript
const user = signal(
  { id: 1, name: 'Alice' },
  { equal: (a, b) => a.id === b.id }
);
Insight per i Colloqui

I candidati che menzionano che i computed signals sono memoizzati e valutati in modo lazy dimostrano una comprensione oltre l'uso base dell'API. Discutere quando usare linkedSignal vs computed mostra familiarità con i pattern di Angular 19+.

Change Detection: Signals e Angular Zoneless

Angular 22 usa di default la change detection zoneless per i nuovi progetti. I signals sono centrali in questo: notificano Angular precisamente quando lo stato cambia, eliminando la necessità che Zone.js faccia il patch delle API async.

zoneless-counter.component.tstypescript
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-zoneless-counter',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <p>Count: {{ count() }}</p>
    <button (click)="increment()">+1</button>
  `
})
export class ZonelessCounterComponent {
  count = signal(0);
  
  increment(): void {
    // L'aggiornamento del signal attiva la change detection
    // Zone.js non è richiesto
    this.count.update(c => c + 1);
  }
}

Con i signals, Angular traccia esattamente quali component dipendono da quale stato. Un aggiornamento del signal schedula la change detection solo per i component interessati, non per l'intero albero.

Resource API: Dati Async come Signals

La Resource API di Angular 20 (resource() e rxResource()) carica dichiarativamente i dati async nei signals, sostituendo i pattern di sottoscrizione manuale.

user-profile.component.tstypescript
import { Component, signal, computed } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  selector: 'app-user-profile',
  standalone: true,
  template: `
    @if (userResource.isLoading()) {
      <p>Loading...</p>
    }
    @if (userResource.error()) {
      <p>Error: {{ userResource.error() }}</p>
    }
    @if (userResource.value(); as user) {
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
    }
  `
})
export class UserProfileComponent {
  private http = inject(HttpClient);
  
  userId = signal(1);
  
  // Resource async dichiarativa
  // Ricarica quando userId cambia
  userResource = rxResource({
    request: () => this.userId(),
    loader: ({ request: id }) => 
      this.http.get<User>(`/api/users/${id}`)
  });
  
  loadUser(id: number): void {
    this.userId.set(id);
  }
}

La resource traccia automaticamente lo stato di caricamento, gli errori e il valore risolto. Ricarica quando il signal di richiesta cambia.

Best Practice per l'Architettura dei Signal

Strutturare i signals in applicazioni più grandi richiede disciplina. Questi pattern scalano dai component ai service.

Raggruppare i signals correlati: Raggruppare i signals che cambiano insieme. Un component form mantiene i suoi field signals insieme, non sparsi tra i service.

Elevare lo stato condiviso ai service: Quando più component necessitano dello stesso stato, spostare il signal in un service iniettabile:

cart.service.tstypescript
import { Injectable, signal, computed } from '@angular/core';

interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

@Injectable({ providedIn: 'root' })
export class CartService {
  private items = signal<CartItem[]>([]);
  
  // Esporre un signal di sola lettura ai consumer
  readonly cartItems = this.items.asReadonly();
  
  readonly totalPrice = computed(() =>
    this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
  );
  
  readonly itemCount = computed(() =>
    this.items().reduce((sum, item) => sum + item.quantity, 0)
  );
  
  addItem(item: Omit<CartItem, 'quantity'>): void {
    this.items.update(items => {
      const existing = items.find(i => i.id === item.id);
      if (existing) {
        return items.map(i => 
          i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
        );
      }
      return [...items, { ...item, quantity: 1 }];
    });
  }
}

Preferire computed rispetto a effect: Se un valore può essere derivato, usare computed. Gli effects sono per i side effects che lasciano il grafo reattivo: manipolazione DOM, localStorage, analytics, richieste di rete.

Migrazione da RxJS a Signals

Le codebase Angular legacy possono migrare in modo incrementale. Le utility di interoperabilità RxJS permettono la coesistenza di signals e observables.

typescript
// Prima: basato su RxJS
@Component({ ... })
export class LegacyComponent {
  private userSubject = new BehaviorSubject<User | null>(null);
  user$ = this.userSubject.asObservable();
  
  userName$ = this.user$.pipe(
    map(u => u?.name ?? 'Guest')
  );
}

// Dopo: basato su Signal
@Component({ ... })
export class ModernComponent {
  user = signal<User | null>(null);
  
  userName = computed(() => this.user()?.name ?? 'Guest');
}

Per le operazioni async, mantenere RxJS per la pipeline e convertire al confine:

typescript
// Approccio ibrido
private search$ = toObservable(this.searchQuery).pipe(
  debounceTime(300),
  switchMap(q => this.api.search(q))
);

results = toSignal(this.search$, { initialValue: [] });

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Prepararsi alle Domande sui Signal nei Colloqui Angular

  • I signals sono primitive sincrone; RxJS gestisce gli stream async
  • I computed signals sono lazy e memoizzati, ricalcolano solo quando le dipendenze cambiano
  • Gli effects vengono eseguiti in modo asincrono durante la change detection, non immediatamente
  • linkedSignal risolve il problema dello stato derivato scrivibile senza effects
  • I signal input (input(), model()) sostituiscono i decoratori con alternative reattive
  • toSignal e toObservable collegano signals e RxJS per la migrazione incrementale
  • Angular zoneless si basa sui signals per la change detection fine-grained
  • La Resource API (rxResource) gestisce lo stato async in modo dichiarativo
  • La guida ufficiale Angular sui signals rimane il riferimento autorevole per i dettagli dell'API e le best practice
Sfida del giorno

Sapresti trovare il bug in Angular?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 17 settembre 2026

Tag

#angular
#signals
#computed
#reactivity
#interview

Condividi

Articoli correlati