Todos conocemos la historia detrás de la necesidad de completar las suscripciones cuando el componente está siendo destruido, de lo contrario, introduciremos fugas de memoria y la máquina de nuestra aplicación/navegador/cliente se volverá cada vez más lenta, debido a las cargas de basura dentro de la memoria.
Ha habido un montón de técnicas desde que el Patrón Observable se hizo lo suficientemente popular como para ser enviado con la versión más reciente de Angular a la vez (la versión 2 que cambió el juego, ahora ya es la 16! ¿Puedes creerlo?):
Usando la instancia Subscription
teníamos que declarar una nueva instancia Subscription
, asignar la actual y llamar a su método unsubscribe
cuando el componente había sido destruido. Este enfoque tenía sus propias limitaciones, ya que había que declarar atributos de suscripción separados por cada asignación, como se muestra en el ejemplo siguiente:
import { Component, OnDestroy, OnInit } from '@angular/core';
import { of, Subscription } from 'rxjs';
@Component({
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss'],
})
export class NavigationComponent implements OnInit, OnDestroy {
subscription: Subscription = new Subscription();
ngOnInit(): void {
this.subscription = of([]).subscribe();
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
Usando el array de suscripciones
En este escenario, somos capaces de aprovechar tantas suscripciones como queramos con un único atributo de suscripciones. (No está mal, ¿verdad?) Pero si pensamos en código limpio y legibilidad, esta tampoco parece ser una opción ideal, ya que nuestro código ya está siendo envuelto con el método push array, y aún así hay que hacer una sangría.
Recuerda que si quieres aprender angular tenemos Workshop
Con un ejemplo simple como el siguiente no parece ser un problema, pero imagina algunas operaciones más complejas en múltiples flujos. No hay que olvidar, que tenemos que cancelar la suscripción de cada (sub) cuando el componente está siendo destruido así:
import { Component, OnDestroy, OnInit } from '@angular/core';
import { of, Subscription } from 'rxjs';
@Component({
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss'],
})
export class NavigationComponent implements OnInit, OnDestroy {
subscriptions: Subscription[] = [];
ngOnInit(): void {
this.subscriptions.push(of([]).subscribe());
}
ngOnDestroy(): void {
this.subscriptions
.forEach((subscription: Subscription) => subscription.unsubscribe());
}
}
Uso del operador takeUntil rxjs combinado con Subject
En lugar de la instancia de suscripción. Ahora utilizamos una nueva instancia de sujeto. He visto varios sabores diferentes de declaración de tipo utilizado dentro. Creo que el más preciso es void
ya que realmente no necesitamos un valor real, sólo una emisión en sí.
Sí aún no sabes de angular. TENEMOS CURSO
A continuación, tenemos que recordar para conectar takeUntil
operador en el pipe. Y asegúrate de que será el último, porque si estás usando otro operador que devuelva un observable de orden superior como switchMap
, puede que no se complete tu flujo, switchMap
seguirá funcionando. Con esta técnica, seguimos manteniendo el lifecycle-hook
de ngOnDestroy
, pero esta vez, llamando al siguiente método de nuestro subject
.
No hay necesidad de completar el flujo del sujeto, ya que no hay una suscripción real a él. Por lo tanto, no hay necesidad de llamar a: this.destroy.complete()
en absoluto.
P.D. Si no está seguro de si su suscripción se ha completado, siempre puede utilizar el operador finalize rxjs, que se activará cuando su suscripción se complete. ¡Listo! Estamos a salvo :-)
import { Component, OnDestroy, OnInit } from '@angular/core';
import { of, Subject, Subscription, takeUntil } from 'rxjs';
@Component({
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss'],
})
export class NavigationComponent implements OnInit, OnDestroy {
private readonly destroy: Subject<void> = new Subject<void>();
ngOnInit(): void {
of([])
.pipe(takeUntil(this.destroy.asObservable()))
.subscribe();
}
ngOnDestroy(): void {
this.destroy.next();
}
}
No tengo que recordar, que esas actividades tienen que ser replicadas dentro de cada componente, ¡donde las suscripciones están siendo usadas! Parece una actividad adicional que realizar y código extra que añadir y recordar.
Como desarrolladores, queremos mejorar constantemente y hacer nuestra vida lo más fácil posible. Ya he visto implementaciones en las que se introduce un componente base sólo para mantener la implementación de la suscripción en un único lugar. Para ser honesto, personalmente no estoy muy de acuerdo con este enfoque, ya que estamos introduciendo una capa adicional de abstracción y herencia que será enviada a cada componente, complicando las pruebas unitarias y trayendo bits adicionales para las llamadas al super constructor.
Esto también abre la caja de pandora, para los desarrolladores menos experimentados para añadir algo más a este componente base (¡Siempre hay algo que añadir!).
También he visto algunas implementaciones usando @Decorators
y operadores rxjs personalizados, que es un enfoque definitivamente más ligero, pero entonces te ves obligado a mantenerlo por tu cuenta y averiguar el enfoque para reutilizarlo en múltiples proyectos.
Ya existe una librería de Netanel Basal llamada @ngneat/until-destroy
que aportó una buena experiencia a nuestra base de código. Ya lo he utilizado en un par de proyectos también. Una observación por mi parte, realmente hay que entender cómo funciona para beneficiarse de ella, de lo contrario fugas de memoria, ¡allá vamos!
¡Finalmente, Angular 16 fue lanzado con un increíble proveedor de DestroyRef! Que se puede utilizar fácilmente, no necesitamos mucho, sólo un token de inyección con DestroyRef
y el operador takeUntilDestroyed
directamente del paquete @angular/core/rxjs-interop
. ¡Tienes que verlo en acción! Ya no es necesario usar el lifecycle-hook ngOnDestroy
para darse de baja.
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
import { of } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss'],
})
export class NavigationComponent implements OnInit {
private readonly destroy: DestroyRef = inject(DestroyRef);
ngOnInit(): void {
of([])
.pipe(takeUntilDestroyed(this.destroy))
.subscribe();
}
}
Finalmente es importante que sepas que 💡 incluso puedes ir más allá y crear tu propio operador Rxjs, ¡que será súper sencillo y fácil de mantener! Utiliza DestroyRef
para crear un operador llamado untilDestroyed
, por ejemplo. A continuación, puede utilizar una cadena de herramientas de código abierto como Bit para generar su documentación de forma automática, y luego publicar, versionar y reutilizarlo en todos sus proyectos con un simple comando bit import your.username/untilDestroyed.