Skip to content

La barrera de la gesti�n del ciclo de vida

Una de las mayores trampas de RxJS es la gesti�n del ciclo de vida. Equivocarse en "cu�ndo hacer subscribe" y "cu�ndo hacer unsubscribe" puede causar fugas de memoria y bugs.

�Cu�ndo se debe hacer subscribe?

Principio b�sico: No hacer subscribe hasta el �ltimo momento

L Mal ejemplo: Hacer subscribe en el medio

typescript
import { interval } from 'rxjs';

function getEvenNumbers() {
  const numbers$ = interval(1000);

  // Hacer subscribe dentro de esto
  numbers$.subscribe(n => {
    if (n % 2 === 0) {
      console.log(n); // �C�mo pasar esto al exterior?
    }
  });
}

 Buen ejemplo: Devolver Observable, hacer subscribe en el lado que llama

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

function getEvenNumbers() {
  return interval(1000).pipe(
    filter(n => n % 2 === 0),
    take(5)
  );
}

// Hacer subscribe en el lado de uso
const subscription = getEvenNumbers().subscribe(n => {
  console.log(n);
});

=� Explicaci�n

  • Mal ejemplo: Al hacer subscribe dentro de la funci�n, se pierde el control (no se puede cancelar, no se puede componer)
  • Buen ejemplo: Al devolver un Observable, el lado que llama puede controlarlo

subscribe es el disparador de "efectos secundarios"

L Mal ejemplo: Ejecutar m�ltiples efectos secundarios dentro de subscribe

typescript
import { fromEvent } from 'rxjs';
import { map } from 'rxjs';

const button = document.querySelector('button')!;

fromEvent(button, 'click')
  .pipe(map(() => Math.random()))
  .subscribe(randomValue => {
    // Efecto secundario 1: Operaci�n DOM
    document.querySelector('#result')!.textContent = randomValue.toString();

    // Efecto secundario 2: Llamada a API
    fetch('/api/log', {
      method: 'POST',
      body: JSON.stringify({ value: randomValue })
    });

    // Efecto secundario 3: LocalStorage
    localStorage.setItem('lastValue', randomValue.toString());
  });

 Buen ejemplo: Separar efectos secundarios, subscribe solo los necesarios

typescript
import { fromEvent } from 'rxjs';
import { map } from 'rxjs';

const button = document.querySelector('button')!;

const randomClicks$ = fromEvent(button, 'click').pipe(
  map(() => Math.random())
);

// Si solo necesitas actualizaci�n DOM
randomClicks$.subscribe(value => {
  document.querySelector('#result')!.textContent = value.toString();
});

// Si solo necesitas logging
randomClicks$.subscribe(value => {
  fetch('/api/log', {
    method: 'POST',
    body: JSON.stringify({ value })
  });
});

=� Explicaci�n

  • subscribe = punto de ejecuci�n de efectos secundarios
  • Si los efectos secundarios son independientes: Separar en m�ltiples subscribe (se puede controlar individualmente)
  • Si los efectos secundarios siempre se ejecutan juntos: OK agruparlos en un solo subscribe
  • Si se necesitan efectos secundarios dentro del pipeline: Usar el operador tap

�Cu�ndo se debe hacer subscribe?: Diagrama de flujo de decisi�n

Visi�n general del ciclo de vida de suscripci�n

El siguiente diagrama de transici�n de estados muestra qu� estados atraviesa una suscripci�n a Observable antes de terminar.

Puntos de gesti�n del ciclo de vida

  • Suscrito: Estado con peligro de fuga de memoria
  • complete/error: Se limpia autom�ticamente (no necesita unsubscribe)
  • unsubscribe: Necesita limpieza manual (especialmente streams infinitos)

�Cu�ndo se debe hacer unsubscribe?

Principio b�sico: Si te suscribes, siempre cancela

L Mal ejemplo: No hacer unsubscribe � Fuga de memoria

typescript
import { interval } from 'rxjs';

const button = document.querySelector('button')!;

function startTimer() {
  interval(1000).subscribe(n => {
    console.log(n);
  });
  // �Esta suscripci�n contin�a eternamente!
}

// Se a�ade una nueva suscripci�n con cada clic del bot�n
button.addEventListener('click', startTimer);
// 10 clics = �10 suscripciones funcionando simult�neamente!

 Buen ejemplo: Cancelar con unsubscribe

typescript
import { interval } from 'rxjs';

function startTimer() {
  const subscription = interval(1000).subscribe(n => {
    console.log(n);
  });

  // Cancelar despu�s de 5 segundos
  setTimeout(() => {
    subscription.unsubscribe();
    console.log('Suscripci�n cancelada');
  }, 5000);
}

=� Explicaci�n

  • Los streams infinitos (interval, fromEvent, etc.) siempre necesitan unsubscribe
  • Sin unsubscribe, fuga de memoria + procesamiento innecesario contin�a

Casos donde unsubscribe no es necesario

 Observables que completan autom�ticamente

typescript
of(1, 2, 3).subscribe(n => console.log(n));
// Despu�s de complete, se limpia autom�ticamente

from([1, 2, 3]).subscribe(n => console.log(n));
// Despu�s de complete, se limpia autom�ticamente

 Cuando la finalizaci�n est� garantizada con take, etc.

typescript
interval(1000).pipe(
  take(5) // complete autom�ticamente despu�s de 5 veces
).subscribe(n => console.log(n));

 Finaliza con error

typescript
throwError(() => new Error('Error')).subscribe({
  error: err => console.error(err)
});

 EMPTY (complete inmediatamente)

typescript
EMPTY.subscribe(() => console.log('No se ejecuta'));

=� Explicaci�n

unsubscribe no es necesario en los siguientes casos

  1. Observables que llaman a complete() - Se limpian autom�ticamente
  2. Cuando se llama a error() - Tambi�n limpieza autom�tica
  3. Cuando la finalizaci�n est� garantizada con take(n), etc. - Se completa expl�citamente

Importante

�Streams infinitos (interval, fromEvent, Subject, etc.) siempre necesitan unsubscribe!

Diagrama de flujo para decidir si unsubscribe es necesario

�Si tienes dudas, hacer unsubscribe es seguro!

Patrones para prevenir fugas de memoria

Patr�n 1: Gesti�n con objeto Subscription

typescript
import { interval, fromEvent } from 'rxjs';
import { Subscription } from 'rxjs';

class MyComponent {
  private subscription = new Subscription();

  ngOnInit() {
    // A�adir m�ltiples suscripciones a un solo Subscription
    this.subscription.add(
      interval(1000).subscribe(n => console.log('Timer:', n))
    );

    this.subscription.add(
      fromEvent(document, 'click').subscribe(() => console.log('Click!'))
    );

    this.subscription.add(
      fromEvent(window, 'resize').subscribe(() => console.log('Resize!'))
    );
  }

  ngOnDestroy() {
    // Cancelar todas las suscripciones de una vez
    this.subscription.unsubscribe();
  }
}

=� Ventajas

  • Gestionar m�ltiples suscripciones con un solo objeto
  • Cancelaci�n en lote en ngOnDestroy
  • F�cil a�adir y eliminar

Patr�n 2: Gesti�n con array

typescript
import { interval, fromEvent } from 'rxjs';
import { Subscription } from 'rxjs';

class MyComponent {
  private subscriptions: Subscription[] = [];

  ngOnInit() {
    this.subscriptions.push(
      interval(1000).subscribe(n => console.log('Timer:', n))
    );

    this.subscriptions.push(
      fromEvent(document, 'click').subscribe(() => console.log('Click!'))
    );
  }

  ngOnDestroy() {
    this.subscriptions.forEach(sub => sub.unsubscribe());
    this.subscriptions = [];
  }
}

=� Ventajas

  • Gesti�n flexible con operaciones de array
  • Tambi�n se puede cancelar individualmente
  • F�cil de depurar (verificar array con console.log)

Patr�n 3: Patr�n takeUntil (recomendado)

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

class MyComponent {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    // A�adir takeUntil(this.destroy$) a todas las suscripciones
    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(n => console.log('Timer:', n));

    fromEvent(document, 'click').pipe(
      takeUntil(this.destroy$)
    ).subscribe(() => console.log('Click!'));

    fromEvent(window, 'resize').pipe(
      takeUntil(this.destroy$)
    ).subscribe(() => console.log('Resize!'));
  }

  ngOnDestroy() {
    // Cancelar todas las suscripciones con un solo next()
    this.destroy$.next();
    this.destroy$.complete();
  }
}

=� Ventajas

  • M�s declarativo - Especifica expl�citamente la condici�n de finalizaci�n en el pipeline
  • No necesita objeto Subscription - Eficiente en memoria
  • F�cil de leer - Al ver el c�digo, se entiende "completa con destroy$"

Gu�a completa del patr�n takeUntil

Patr�n b�sico

typescript
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs';

const destroy$ = new Subject<void>();

// Esta suscripci�n contin�a hasta que destroy$ haga next()
interval(1000).pipe(
  takeUntil(destroy$)
).subscribe(n => console.log(n));

// Detener todas las suscripciones despu�s de 5 segundos
setTimeout(() => {
  destroy$.next();
  destroy$.complete();
}, 5000);

Diagrama de m�rmol

interval(1000):  --0--1--2--3--4--5--6--7-->
destroy$:        ----------X

                       llamada a next()

Resultado takeUntil:   --0--1--2|

                       complete

Aplicar a m�ltiples Observables

typescript
import { interval, fromEvent, timer, Subject } from 'rxjs';
import { takeUntil, map } from 'rxjs';

const destroy$ = new Subject<void>();

// Patr�n: usar el mismo destroy$ para todos los streams
interval(1000).pipe(
  takeUntil(destroy$),
  map(n => `Timer: ${n}`)
).subscribe(console.log);

fromEvent(document, 'click').pipe(
  takeUntil(destroy$),
  map(() => 'Click!')
).subscribe(console.log);

timer(2000).pipe(
  takeUntil(destroy$),
  map(() => 'Timer finished')
).subscribe(console.log);

// Detener todo
function cleanup() {
  destroy$.next();
  destroy$.complete();
}

// Ejemplo: llamar cleanup() en la transici�n de p�gina
window.addEventListener('beforeunload', cleanup);

Errores comunes del patr�n takeUntil

Error 1: La posici�n de takeUntil es incorrecta

L Mal ejemplo: map antes de takeUntil

typescript
import { interval, Subject } from 'rxjs';
import { map, takeUntil } from 'rxjs';

const destroy$ = new Subject<void>();

interval(1000).pipe(
  takeUntil(destroy$),  // Aunque complete aqu�...
  map(n => n * 2)       // map puede ejecutarse
).subscribe(console.log);

 Buen ejemplo: takeUntil al final

typescript
import { interval, Subject } from 'rxjs';
import { map, takeUntil } from 'rxjs';

const destroy$ = new Subject<void>();

interval(1000).pipe(
  map(n => n * 2),
  takeUntil(destroy$)  // Despu�s de todos los operadores
).subscribe(console.log);

=� Explicaci�n

  • takeUntil se coloca al final en la medida de lo posible
  • Excepci�n: A veces se coloca antes de operadores multicast como shareReplay

Error 2: No completar destroy$

L Mal ejemplo: No llamar a complete()

typescript
import { Subject } from 'rxjs';

const destroy$ = new Subject<void>();

function cleanup() {
  destroy$.next();
  // L No se llama a complete()
}

// Problema: destroy$ mismo se convierte en causa de fuga de memoria

 Buen ejemplo: Llamar tanto a next() como a complete()

typescript
import { Subject } from 'rxjs';

const destroy$ = new Subject<void>();

function cleanup() {
  destroy$.next();
  destroy$.complete();
}

=� Explicaci�n

  • Solo con next(), destroy$ permanece suscrito
  • Siempre llamar tambi�n a complete()

Error 3: Intentar reutilizar

L Mal ejemplo: Reutilizar Subject completado

typescript
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs';

const destroy$ = new Subject<void>();

function start() {
  interval(1000).pipe(
    takeUntil(destroy$)
  ).subscribe(console.log);
}

function stop() {
  destroy$.next();
  destroy$.complete();
}

start();
setTimeout(stop, 3000);

// L Problema: destroy$ ya est� completado, por lo que si haces start() de nuevo, termina inmediatamente
setTimeout(start, 5000); // Esto no funciona

 Buen ejemplo: Regenerar destroy$

typescript
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs';

class MyComponent {
  private destroy$ = new Subject<void>();

  start() {
    // Si ya est� completado, regenerar
    if (this.destroy$.closed) {
      this.destroy$ = new Subject<void>();
    }

    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(console.log);
  }

  stop() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

=� Explicaci�n

  • Un Subject no se puede reutilizar una vez completado
  • Si necesitas reiniciar, crear un nuevo Subject

Mejores pr�cticas de gesti�n de Subscription

Mejor pr�ctica 1: Tener destroy$ por unidad de componente/clase

typescript
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs';

class UserProfileComponent {
  private destroy$ = new Subject<void>();

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.userService.getUser().pipe(
      takeUntil(this.destroy$)
    ).subscribe(user => {
      console.log(user);
    });

    this.userService.getUserPosts().pipe(
      takeUntil(this.destroy$)
    ).subscribe(posts => {
      console.log(posts);
    });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

=� Ventajas

  • Consistencia - Mismo patr�n en todos los componentes
  • Mantenibilidad - Al a�adir nuevas suscripciones, no hay cambios en ngOnDestroy
  • Seguridad - No hay olvidos de unsubscribe

Mejor pr�ctica 2: Utilizar AsyncPipe (en caso de Angular)

typescript
import { Component } from '@angular/core';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-user-profile',
  template: `
    <!-- AsyncPipe hace subscribe/unsubscribe autom�ticamente -->
    <div *ngIf="user$ | async as user">
      <h1>{{ user.name }}</h1>
      <p>{{ user.email }}</p>
    </div>

    <ul>
      <li *ngFor="let post of posts$ | async">
        {{ post.title }}
      </li>
    </ul>
  `
})
export class UserProfileComponent {
  user$: Observable<User>;
  posts$: Observable<Post[]>;

  constructor(private userService: UserService) {
    // Pasar Observable directamente al template
    this.user$ = this.userService.getUser();
    this.posts$ = this.userService.getUserPosts();

    // �No necesita ngOnDestroy! AsyncPipe cancela autom�ticamente
  }
}

=� Ventajas

  • unsubscribe autom�tico - Cancelaci�n autom�tica al destruir el componente
  • Compatibilidad OnPush - Detecci�n de cambios optimizada
  • C�digo conciso - No necesita boilerplate de subscribe/unsubscribe

Mejor pr�ctica 3: Cambiar estrategia seg�n larga vida vs corta vida

typescript
import { Injectable } from '@angular/core';
import { BehaviorSubject, interval, fromEvent } from 'rxjs';
import { takeUntil } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class DataService {
  //  Estado compartido en todo el servicio (larga vida)
  // � Mantener suscripci�n hasta finalizaci�n de aplicaci�n
  private userState$ = new BehaviorSubject<User | null>(null);

  getUser() {
    return this.userState$.asObservable();
  }

  // L No hacer subscribe directamente en componente
  //  Suscribir con AsyncPipe o takeUntil
}

class MyComponent {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    //  Suscripci�n vinculada al ciclo de vida del componente (corta vida)
    // � Cancelar siempre en ngOnDestroy
    interval(1000).pipe(
      takeUntil(this.destroy$)
    ).subscribe(n => console.log(n));

    fromEvent(window, 'resize').pipe(
      takeUntil(this.destroy$)
    ).subscribe(() => console.log('Resize'));
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

=� Principios

Tipo de suscripci�nCiclo de vidaM�todo de gesti�n
Estado globalToda la aplicaci�nBehaviorSubject + AsyncPipe
Espec�fico de p�gina/rutaMientras la ruta es v�lidatakeUntil(routeDestroy$)
Espec�fico de componenteMientras existe el componentetakeUntil(destroy$) or AsyncPipe
Llamada API �nicaHasta completartake(1) or first()

Mejor pr�ctica 4: Establecer condiciones de finalizaci�n expl�citas

L Mal ejemplo: No est� claro cu�ndo termina

typescript
import { fromEvent } from 'rxjs';

fromEvent(document, 'click').subscribe(() => {
  console.log('Click');
});

 Buen ejemplo 1: L�mite de veces

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

fromEvent(document, 'click').pipe(
  take(5) // Termina autom�ticamente despu�s de 5 veces
).subscribe(() => {
  console.log('Click (m�ximo 5 veces)');
});

 Buen ejemplo 2: L�mite de tiempo

typescript
import { fromEvent, timer } from 'rxjs';
import { takeUntil } from 'rxjs';

const timeout$ = timer(10000); // Despu�s de 10 segundos
fromEvent(document, 'click').pipe(
  takeUntil(timeout$)
).subscribe(() => {
  console.log('Click (dentro de 10 segundos)');
});

 Buen ejemplo 3: M�ltiples condiciones de finalizaci�n

typescript
import { fromEvent, Subject, merge } from 'rxjs';
import { takeUntil, take } from 'rxjs';

const destroy$ = new Subject<void>();
const maxClicks$ = fromEvent(document, 'click').pipe(take(10));

fromEvent(document, 'mousemove').pipe(
  takeUntil(merge(destroy$, maxClicks$)) // Termina con cualquiera
).subscribe(() => {
  console.log('Mouse move');
});

=� Principios

  • Especificar "cu�ndo termina" expl�citamente - Evitar streams infinitos
  • Establecer condiciones de finalizaci�n con take, first, takeWhile, takeUntil, etc.
  • Vincular al ciclo de vida (destroy$, timeout$, etc.)

Lista de verificaci�n de comprensi�n

Verifica si puedes responder a las siguientes preguntas.

markdown
## Comprensi�n b�sica
- [ ] Puedo explicar qu� sucede al hacer subscribe a un Observable
- [ ] Puedo distinguir casos donde unsubscribe es necesario y no necesario
- [ ] Puedo explicar las causas de fugas de memoria

## Aplicaci�n de patrones
- [ ] Puedo gestionar m�ltiples suscripciones con objeto Subscription
- [ ] Puedo implementar el patr�n takeUntil
- [ ] Puedo colocar destroy$ adecuadamente (�ltimo operador)

## Mejores pr�cticas
- [ ] S� cu�ndo usar AsyncPipe
- [ ] Puedo distinguir y gestionar suscripciones de larga y corta vida
- [ ] Puedo establecer condiciones de finalizaci�n expl�citas

## Depuraci�n
- [ ] Conozco m�todos para detectar fugas de memoria
- [ ] Puedo encontrar olvidos de unsubscribe
- [ ] Puedo verificar el n�mero de suscripciones con DevTools del navegador

Pr�ximos pasos

Una vez que entiendas la gesti�n del ciclo de vida, aprende sobre selecci�n de operadores.

La confusi�n de selecci�n de operadores - Criterios para elegir el apropiado entre m�s de 100 operadores

P�ginas relacionadas

<� Ejercicios de pr�ctica

Problema 1: Corregir fuga de memoria

El siguiente c�digo tiene una fuga de memoria. Corr�gelo.

typescript
class ChatComponent {
  ngOnInit() {
    interval(5000).subscribe(() => {
      this.chatService.checkNewMessages().subscribe(messages => {
        console.log('Nuevos mensajes:', messages);
      });
    });
  }
}
Ejemplo de respuesta
typescript
class ChatComponent {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    interval(5000).pipe(
      takeUntil(this.destroy$),
      switchMap(() => this.chatService.checkNewMessages())
    ).subscribe(messages => {
      console.log('Nuevos mensajes:', messages);
    });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Puntos de correcci�n

  1. A�adir Subject destroy$
  2. Detener interval con takeUntil(this.destroy$)
  3. Resolver subscribe anidado con switchMap
  4. Hacer cleanup en ngOnDestroy

Problema 2: Selecci�n de patr�n adecuado

Para los siguientes escenarios, elige el patr�n de gesti�n de suscripci�n m�s �ptimo.

  1. Petici�n HTTP (solo una vez)
  2. Conexi�n WebSocket (durante la existencia del componente)
  3. Estado global de usuario (toda la aplicaci�n)
Ejemplo de respuesta

1. Petici�n HTTP (solo una vez)

typescript
//  take(1) o first() - complete autom�tico despu�s de una vez
this.http.get('/api/user').pipe(
  take(1)
).subscribe(user => console.log(user));

// O AsyncPipe (en caso de Angular)
user$ = this.http.get('/api/user');

2. Conexi�n WebSocket (durante la existencia del componente)

typescript
//  Patr�n takeUntil - Desconectar al destruir componente
private destroy$ = new Subject<void>();

ngOnInit() {
  this.websocket.connect().pipe(
    takeUntil(this.destroy$)
  ).subscribe(message => console.log(message));
}

ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

3. Estado global de usuario (toda la aplicaci�n)

typescript
//  BehaviorSubject + AsyncPipe - No necesita unsubscribe
@Injectable({ providedIn: 'root' })
class AuthService {
  private userState$ = new BehaviorSubject<User | null>(null);

  getUser() {
    return this.userState$.asObservable();
  }
}

// Uso en componente
user$ = this.authService.getUser(); // Suscribir con AsyncPipe

Publicado bajo licencia CC-BY-4.0.