El conocimiento es el nuevo dinero.
Aprender es la nueva manera en la que inviertes
Acceso Cursos

Cómo DestroyRef me ha facilitado la vida con Angular 16

· 4 min de lectura
Cómo DestroyRef me ha facilitado la vida con Angular 16

Ya no es necesario utilizar el lifecycle-hook ngOnDestroy para darse de baja.

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 Observable Pattern se hizo lo suficientemente popular como para ser enviado con la versión más reciente de Angular en un momento (la versión 2 que cambió el juego, ¡ahora ya es la 16! ¿Puedes creerlo?):

  • Usando una instancia de Suscripción: Tuvimos que declarar una nueva instancia de Suscripción, asignar la actual y llamar a su método de desuscripción cuando el componente había sido destruido. Este enfoque tenía sus propias limitaciones, ya que habría que declarar atributos de suscripción separados por asignación, como se presenta 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. 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í.  

A continuación, tenemos que recordar para conectar takeUntil operador en la tubería. 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 (hay un par de técnicas para abordarlo también, pero es una historia para otro artículo).

Ya existe una librería de Netanel Basal llamada @ngneat/until-destroy que ha aportado 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 DestroyRefy el operador takeUntilDestroyed directamente del paquete @angular/core/rxjs-interop. ¡Tienes que verlo en acción! Ya no hace falta 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();
  }
}

💡 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.

Fuente

Plataforma de cursos gratis sobre programación

Artículos Relacionados

Transiciones en el Router Angular 17
· 3 min de lectura
Errores comunes de RxJS en Angular
· 4 min de lectura
¿Por qué evitar usar 'any' en TypeScript?
· 5 min de lectura
Module Federation Dinámico con Angular
· 7 min de lectura