Skip to content

Barriere alla comprensione concettuale ​

La prima barriera a RxJS è la comprensione dei concetti. Il comportamento degli Observable può essere controintuitivo, specialmente per gli sviluppatori abituati alle Promise.

Differenze essenziali tra Observable e Promise ​

Promise: elaborazione asincrona una tantum ​

typescript
// Promise: restituisce il valore una sola volta
const userPromise = fetch('/api/user/1').then(res => res.json());

userPromise.then(user => console.log(user)); // Ottiene le informazioni utente una sola volta
userPromise.then(user => console.log(user)); // Stesso risultato dalla cache

Caratteristiche

  • Eager (esecuzione immediata) - L'elaborazione inizia al momento della creazione della Promise
  • Completa solo una volta - Solo una volta, successo o fallimento
  • Non cancellabile - Una volta avviata, non può essere interrotta
  • Sempre Hot - Più then condividono lo stesso risultato

Observable: stream (flusso di dati con asse temporale) ​

typescript
import { Observable } from 'rxjs';

// Observable: più valori che fluiscono lungo l'asse temporale
const user$ = new Observable<User>(subscriber => {
  console.log('Esecuzione Observable iniziata!');
  fetch('/api/user/1')
    .then(res => res.json())
    .then(user => {
      subscriber.next(user);
      subscriber.complete();
    });
});

// ❌ Non succede ancora nulla a questo punto (Lazy)
console.log('Creazione Observable completata');

// ✅ Eseguito solo dopo la sottoscrizione
user$.subscribe(user => console.log('Sottoscrizione 1:', user));
user$.subscribe(user => console.log('Sottoscrizione 2:', user));
// → La chiamata API viene eseguita due volte (Cold Observable)

Output ​

Creazione Observable completata
Esecuzione Observable iniziata!
Sottoscrizione 1: { id: 1, name: 'Alice' }
Esecuzione Observable iniziata!
Sottoscrizione 2: { id: 1, name: 'Alice' }

Caratteristiche

  • Lazy (esecuzione ritardata) - Non succede nulla finché non si sottoscrive
  • Più valori possono fluire - next() può essere chiamato più volte
  • Cancellabile - Può essere interrotto con unsubscribe
  • Cold o Hot - Si può scegliere di eseguire per sottoscrizione o condividere

Tabella di confronto ​

CaratteristicaPromiseObservable
Tempistica di esecuzioneImmediata (Eager)Alla sottoscrizione (Lazy)
Numero di valoriSolo una volta0 o più (multipli possibili)
CancellazioneNon possibilePossibile (unsubscribe)
RiutilizzoRisultato dalla cacheRiesecuzione per sottoscrizione (Cold)
Dopo erroreTerminaTermina (retry possibile)

Visualizzazione delle differenze di comportamento ​

Il seguente diagramma di sequenza illustra le differenze nel flusso di esecuzione tra Promise e Observable.

Idee sbagliate comuni ​

❌ Idea sbagliata 1: "Observable è solo per l'asincrono" ​

typescript
// Observable può gestire anche elaborazioni sincrone
import { of } from 'rxjs';

const sync$ = of(1, 2, 3);

console.log('Prima di subscribe');
sync$.subscribe(value => console.log(value));
console.log('Dopo subscribe');

// Output (eseguito sincronamente):
// Prima di subscribe
// 1
// 2
// 3
// Dopo subscribe

❌ Idea sbagliata 2: "subscribe restituisce un valore" ​

typescript
import { map, of } from "rxjs";

const observable$ = of(1, 2, 3);

// ❌ Cattivo esempio: pensiero da Promise
const value = observable$.subscribe(x => x); // value è un oggetto Subscription
console.log(value); // Subscription { ... } ← Non il valore atteso

// ✅ Buon esempio: pensiero da Observable
observable$.pipe(
  map(x => x * 2)
).subscribe(value => {
  console.log(value); // Usa il valore qui
});

Comprensione intuitiva di Cold vs Hot ​

Cold Observable: stream indipendenti per sottoscrizione ​

typescript
import { interval } from 'rxjs';
import { take } from 'rxjs';

// Cold: ogni sottoscrittore ha un timer indipendente
const cold$ = interval(1000).pipe(take(3));

console.log('Sottoscrizione 1 avviata');
cold$.subscribe(x => console.log('Sottoscrizione 1:', x));

setTimeout(() => {
  console.log('Sottoscrizione 2 avviata (dopo 2 secondi)');
  cold$.subscribe(x => console.log('Sottoscrizione 2:', x));
}, 2000);

// Output:
// Sottoscrizione 1 avviata
// Sottoscrizione 1: 0        (dopo 1 secondo)
// Sottoscrizione 1: 1        (dopo 2 secondi)
// Sottoscrizione 2 avviata (dopo 2 secondi)
// Sottoscrizione 1: 2        (dopo 3 secondi)
// Sottoscrizione 2: 0        (dopo 3 secondi) ← Sottoscrizione 2 parte dall'inizio
// Sottoscrizione 2: 1        (dopo 4 secondi)
// Sottoscrizione 2: 2        (dopo 5 secondi)

Caratteristiche Cold

  • Esecuzione indipendente per sottoscrizione
  • Mantiene un "blueprint" dei dati
  • Esempi: chiamate API HTTP, timer, lettura file

Hot Observable: tutti i sottoscrittori condividono lo stesso stream ​

typescript
import { interval } from 'rxjs';
import { take, share } from 'rxjs';

// Hot: condiviso da share()
const hot$ = interval(1000).pipe(
  take(3),
  share() // Questo lo rende Hot
);

console.log('Sottoscrizione 1 avviata');
hot$.subscribe(x => console.log('Sottoscrizione 1:', x));

setTimeout(() => {
  console.log('Sottoscrizione 2 avviata (dopo 2 secondi)');
  hot$.subscribe(x => console.log('Sottoscrizione 2:', x));
}, 2000);

// Output:
// Sottoscrizione 1 avviata
// Sottoscrizione 1: 0        (dopo 1 secondo)
// Sottoscrizione 1: 1        (dopo 2 secondi)
// Sottoscrizione 2 avviata (dopo 2 secondi)
// Sottoscrizione 1: 2        (dopo 3 secondi)
// Sottoscrizione 2: 2        (dopo 3 secondi) ← Sottoscrizione 2 si è unita a metà

Caratteristiche Hot

  • Tutti i sottoscrittori condividono la stessa esecuzione
  • I dati vengono "trasmessi"
  • Esempi: eventi click, WebSocket, Subject

Come identificare Cold/Hot ​

typescript
import { fromEvent, interval, of, Subject } from 'rxjs';
import { share } from 'rxjs';

// Cold (esecuzione indipendente per sottoscrizione)
const cold1$ = of(1, 2, 3);
const cold2$ = interval(1000);
const cold3$ = ajax('/api/data');
const cold4$ = fromEvent(button, 'click'); // Cold ma speciale

// Hot (condiviso tra sottoscrittori)
const hot1$ = new Subject<number>();
const hot2$ = interval(1000).pipe(share()); // Converte Cold in Hot

Come distinguere

  • Creation Functions (of, from, fromEvent, interval, ajax, ecc.) → Cold
  • Famiglia Subject → Hot
  • Uso di share(), shareReplay() → Converte Cold in Hot

Passaggio al pensiero dichiarativo ​

Imperativo vs Dichiarativo ​

RxJS è un paradigma di programmazione dichiarativa.

❌ Pensiero imperativo (Promise/async-await) ​

typescript
// Imperativo: descrive "come" elaborare
async function processUsers() {
  const response = await fetch('/api/users');
  const users = await response.json();

  const activeUsers = [];
  for (const user of users) {
    if (user.isActive) {
      activeUsers.push(user);
    }
  }

  const userNames = [];
  for (const user of activeUsers) {
    userNames.push(user.name.toUpperCase());
  }

  return userNames;
}

✅ Pensiero dichiarativo (RxJS) ​

typescript
import { from } from 'rxjs';
import { mergeMap, filter, map, toArray } from 'rxjs';

// Dichiarativo: descrive "cosa" trasformare
const processUsers$ = from(fetch('/api/users')).pipe(
  mergeMap(res => res.json()),
  mergeMap(users => users), // Espande l'array
  filter(user => user.isActive),
  map(user => user.name.toUpperCase()),
  toArray()
);

processUsers$.subscribe(userNames => console.log(userNames));

Differenza

  • Imperativo: descrive la procedura (loop, ramificazione condizionale, assegnazione variabili)
  • Dichiarativo: descrive la pipeline di trasformazione (flusso di dati)

Punti chiave per la trasformazione del pensiero ​

Punto 1: Nessuna elaborazione dati dentro subscribe ​

La trasformazione dei dati dovrebbe essere in pipe, subscribe solo per gli effetti collaterali.

typescript
import { filter, map, of } from "rxjs";

const observable$ = of(1, 2, 3);
// ❌ Cattivo esempio: elaborazione dentro subscribe
observable$.subscribe(value => {
  const doubled = value * 2;           // 👈 Calcolo dentro subscribe
  const filtered = doubled > 4 ? doubled : null;  // 👈 Ramificazione condizionale dentro subscribe
  if (filtered) {                      // 👈 if dentro subscribe
    console.log(filtered);
  }
});

// ✅ Buon esempio: trasformazione in pipe
observable$.pipe(
  map(value => value * 2),       // Calcolo in pipe
  filter(value => value > 4)     // Anche il filtraggio in pipe
).subscribe(value => console.log(value));  // subscribe solo per effetti collaterali

Punto 2: Nessuna variabile intermedia ​

typescript
import { filter, map, Observable, of } from "rxjs";

const source$ = of(1, 2, 3, 4, 5);

// ❌ Cattivo esempio: salva in variabili intermedie
let doubled$: Observable<number>;      // 👈 Dichiara variabile intermedia
let filtered$: Observable<number>;     // 👈 Dichiara variabile intermedia

doubled$ = source$.pipe(map(x => x * 2));    // 👈 Assegna a variabile intermedia
filtered$ = doubled$.pipe(filter(x => x > 5)); // 👈 Assegna a variabile intermedia
filtered$.subscribe(console.log);

// ✅ Buon esempio: connetti con pipeline
source$.pipe(
  map(x => x * 2),      // Connetti direttamente nella pipeline
  filter(x => x > 5)    // Connetti direttamente nella pipeline
).subscribe(console.log);

Punto 3: Evita subscribe annidati ​

typescript
// ❌ Cattivo esempio: subscribe annidato
getUser$(userId).subscribe(user => {
  getOrders$(user.id).subscribe(orders => {  // 👈 Altro subscribe dentro subscribe (annidato)
    console.log(user, orders);
  });  // 👈 L'unsubscribe diventa complicato
});

// ✅ Buon esempio: appiattisci con mergeMap
getUser$(userId).pipe(
  mergeMap(user =>                // Appiattisci l'Observable interno con mergeMap
    getOrders$(user.id).pipe(
      map(orders => ({ user, orders }))
    )
  )
).subscribe(({ user, orders }) => console.log(user, orders));  // Sottoscrizione solo in un posto

Punto 4: Organizza con sintassi di separazione in 3 fasi ​

Una tecnica importante che migliora significativamente la leggibilità e la manutenibilità del codice RxJS è la sintassi di separazione in fasi.

typescript
// ❌ Cattivo esempio: one-liner con tutto mescolato
fromEvent(document, 'click').pipe(
  map(event => (event as MouseEvent).clientX),
  filter(x => x > 100),
  throttleTime(200)
).subscribe({
  next: x => console.log('Posizione click:', x),
  error: err => console.error(err)
});

Problemi

  • Definizione stream, trasformazione e sottoscrizione mescolate
  • Difficile da debuggare (non si sa dove si verificano i problemi)
  • Difficile da testare
  • Non riutilizzabile
typescript
// ✅ Buon esempio: separazione in 3 fasi

import { filter, fromEvent, map, throttleTime } from "rxjs";

// 1. Definizione Observable (sorgente dello stream)
const clicks$ = fromEvent(document, 'click');

// 2. Definizione Pipeline (processo di trasformazione dati)
const processed$ = clicks$.pipe(
  map(event => (event as MouseEvent).clientX),
  filter(x => x > 100),
  throttleTime(200)
);

// 3. Processo di sottoscrizione (esecuzione effetti collaterali)
const subscription = processed$.subscribe({
  next: x => console.log('Posizione click:', x),
  error: err => console.error(err),
  complete: () => console.log('Completato')
});

Vantaggi ​

  • Facile da debuggare - Si possono inserire console.log e tap ad ogni fase
  • Testabile - processed$ può essere testato indipendentemente
  • Riutilizzabilità - clicks$ e processed$ possono essere usati altrove
  • Migliore leggibilità - L'intento del codice è chiaro

La sintassi di separazione in fasi è uno dei metodi più pratici per superare le difficoltà di RxJS.

Per maggiori informazioni, vedi Capitolo 10: One-liner hell e sintassi di separazione in fasi.

Sperimenta e comprendi (uso dello Starter Kit) ​

Esperimento 1: Differenza tra Lazy e Eager ​

typescript
import { Observable } from 'rxjs';

console.log('=== Promise (Eager) ===');
const promise = new Promise((resolve) => {
  console.log('Promise eseguita!');
  resolve(42);
});
console.log('Creazione Promise completata');
promise.then(value => console.log('Risultato Promise:', value));

console.log('\n=== Observable (Lazy) ===');
const observable$ = new Observable(subscriber => {
  console.log('Observable eseguito!');
  subscriber.next(42);
  subscriber.complete();
});
console.log('Creazione Observable completata');
observable$.subscribe(value => console.log('Risultato Observable:', value));

// Output:
// === Promise (Eager) ===
// Promise eseguita!
// Creazione Promise completata
// Risultato Promise: 42
//
// === Observable (Lazy) ===
// Creazione Observable completata
// Observable eseguito!
// Risultato Observable: 42

Esperimento 2: Differenza tra Cold e Hot ​

typescript
import { interval } from 'rxjs';
import { take, share } from 'rxjs';

// Cold: indipendente per sottoscrizione
const cold$ = interval(1000).pipe(take(3));

console.log('Cold Observable:');
cold$.subscribe(x => console.log('Cold Sottoscrizione 1:', x));
setTimeout(() => {
  cold$.subscribe(x => console.log('Cold Sottoscrizione 2:', x));
}, 2000);

// Hot: condiviso
const hot$ = interval(1000).pipe(take(3), share());

setTimeout(() => {
  console.log('\nHot Observable:');
  hot$.subscribe(x => console.log('Hot Sottoscrizione 1:', x));
  setTimeout(() => {
    hot$.subscribe(x => console.log('Hot Sottoscrizione 2:', x));
  }, 2000);
}, 6000);

Eseguilo nell'ambiente di apprendimento per sperimentare la differenza.

Esperimento 3: Dichiarativo vs Imperativo ​

typescript
import { of } from 'rxjs';
import { map, filter } from 'rxjs';

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Imperativo
console.log('=== Imperativo ===');
const result1: number[] = [];
for (const num of numbers) {
  const doubled = num * 2;
  if (doubled > 10) {
    result1.push(doubled);
  }
}
console.log(result1);

// Dichiarativo
console.log('\n=== Dichiarativo ===');
of(...numbers).pipe(
  map(num => num * 2),
  filter(num => num > 10)
).subscribe(num => console.log(num));

Verifica della comprensione ​

Verifica di poter rispondere alle seguenti domande.

markdown
## Concetti di base
- [ ] Elencare tre differenze tra Promise e Observable
- [ ] Spiegare la differenza tra Lazy e Eager
- [ ] Spiegare la differenza tra Cold e Hot con esempi

## Pratica
- [ ] Spiegare perché l'elaborazione non dovrebbe essere completata dentro subscribe
- [ ] Sapere come correggere un subscribe annidato
- [ ] Conoscere come convertire un Cold Observable in Hot

## Debug
- [ ] Identificare la causa quando un Observable non viene eseguito
- [ ] Comprendere cosa causa l'esecuzione multipla di una sottoscrizione

Prossimi passi ​

Una volta compresa la teoria, è il momento di passare alle barriere pratiche.

  • Barriere alla gestione del ciclo di vita - Quando fare subscribe/unsubscribe
  • Confusione nella selezione degli operatori - Criteri per scegliere tra 100+ operatori

Sezioni correlate ​

Pubblicato sotto licenza CC-BY-4.0.