Categorie
Angular JavaScript RxJS

Memoria RxJS: come gestirla correttamente

RxJS è una delle librerie più usate in un’applicazione sviluppata con Angular.

Solitamente, si fa uso di questa libreria per implementare la comunicazioni tra oggetti diversi, siano essi componenti o servizi.

Un tipico pattern utilizzato nello sviluppo dei servizi Angular è il seguente:

export class SomeService() {
  constructor() {}

  private somethingSubject$: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
  something$: Observable<string[]> = this.somethingSubject$.asObservable();

  updateSomething(aNewSomething: string[]): void {
    this.somethingSubject$.next(aNewSomething);
  }
}

Memoria RxJS: un esempio pratico

Un esempio di utilizzo di questo pattern è l’implementazione di un componente che sia in grado di ricevere gli aggiornamenti di SomeService effettuando una sottoscrizione a something$.

export class SomeComponent() implements OnInit, OnDestroy {
  private subscription!: Subscription;  

  constructor(private someService: SomeService) {}  ​

 ​ ngOnInit(): void {
    this.subscription = this.someService.something$
		.subscribe((status: string[]) => {
			// qui facciamo qualcosa con status
		});
 ​ }
 
  ngOnDestroy(): void {
        if (subscription) {
            subscribe.unsubscribe();
        }
  }
}

Gli eventuali problemi nella gestione della memoria possono sorgere nella gestione del valore ricevuto da something$ (i.e. nel contesto della Subscription).

Per come abbiamo progettato il servizio, infatti, si potrebbe pensare che l’unico modo per aggiornare il valore di something$ sia utilizzare il metodo updateSomething(aNewSomething: string[ ]) di SomeService.

Invece non è così.

Comprendere il problema

Consideriamo ora il seguente codice, che evidenzia quanto sia importante gestire correttamente la memoria in RxJS.

import { BehaviorSubject, Observable } from 'rxjs';
import { take } from 'rxjs/operators';

const behaviorSubject: BehaviorSubject<string[]> = new BehaviorSubject([]);

behaviorSubject.next(['Cane']);

const subOne = behaviorSubject.subscribe((value) => {
  console.log('primaSubscription', value);
});

let valueFromBehavior = behaviorSubject.getValue();

valueFromBehavior.push('Gatto');

const subTwo = behaviorSubject.subscribe((value) => {
  console.log('secondaSubscription', value);
});

const observable: Observable<string[]> = behaviorSubject.asObservable();

observable.pipe(take(1)).subscribe((payload: string[]) => {
  payload.push('Topo');
});

const subThree = behaviorSubject.subscribe((value) => {
  console.log('terzaSubscription', value);
});

Questo codice, se eseguito, scriverà nella console del browser il seguente testo:

primaSubscription
["Cane"]
secondaSubscription
["Cane", "Gatto"]
terzaSubscription
["Cane", "Gatto", "Topo"]

Da questo codice (disponibile su StackBlitz) emerge che sia possibile modificare il valore di behaviorSubject senza utilizzare il metodo next() del behaviorSubject.

Infatti, sia modificando l’array ottenuto utilizzando getValue() che modificando quello ottenuto da observable, si modifica il valore contenuto in behaviorSubject, senza però notificare i vari subscribers.

Come si può quindi procedere nel caso in si voglia:

  1. ottenere l’ultimo valore emesso dal behaviorSubject
  2. modificare tale valore senza influenzare il contenuto del behaviourSubject
  3. comunicare il nuovo valore ai subscribers del behaviorSubject

Gestire la memoria di RxJS efficacemente

Per evitare il comportamento esposto in precedenza, è opportuno far uso di JSON.parse(JSON.stringify(behaviorSubject.getValue())).

In questo modo, si crea una copia dell’oggetto / array contenuto nel behaviorSubject.

import { BehaviorSubject, Observable } from 'rxjs';
import { take } from 'rxjs/operators';

const behaviorSubject: BehaviorSubject<string[]> = new BehaviorSubject([]);

behaviorSubject.next(['Cane']);

const subOne = behaviorSubject.subscribe((value) => {
  console.log('primaSubscription', value);
});

let valueFromBehavior = JSON.parse(JSON.stringify(behaviorSubject.getValue()));

valueFromBehavior.push('Gatto');

const subTwo = behaviorSubject.subscribe((value) => {
  console.log('secondaSubscription', value);
});

const observable: Observable<string[]> = behaviorSubject.asObservable();

observable.pipe(take(1)).subscribe((payload: string[]) => {
  let payloadCopy: string[] = JSON.parse(JSON.stringify(payload));
  payloadCopy.push('Topo');
});

const subThree = behaviorSubject.subscribe((value) => {
  console.log('terzaSubscription', value);
});

let newValueForBehaviour: string[] = JSON.parse(
  JSON.stringify(behaviorSubject.getValue())
);
newValueForBehaviour.push('Airone');

behaviorSubject.next(newValueForBehaviour);

Questo codice (anch’esso disponibile su StackBlitz), se eseguito, produce il seguente output in console:

primaSubscription
["Cane"]
secondaSubscription
["Cane"]
terzaSubscription
["Cane"]
primaSubscription
["Cane", "Airone"]
secondaSubscription
["Cane", "Airone"]
terzaSubscription
["Cane", "Airone"]

In questo caso, il valore di behaviorSubject viene modificato secondo le attese, propagandolo ai subscribers utilizzando la funzione next().

Conclusioni

In questo articolo hai approfondito, tramite esempi pratici, la questione della corretta gestione della memoria in RxJS.

Seguimi su