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
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
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
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
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
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
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
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�ticamenteCuando la finalizaci�n est� garantizada con take, etc.
interval(1000).pipe(
take(5) // complete autom�ticamente despu�s de 5 veces
).subscribe(n => console.log(n));Finaliza con error
throwError(() => new Error('Error')).subscribe({
error: err => console.error(err)
});EMPTY (complete inmediatamente)
EMPTY.subscribe(() => console.log('No se ejecuta'));=� Explicaci�n
unsubscribe no es necesario en los siguientes casos
- Observables que llaman a complete() - Se limpian autom�ticamente
- Cuando se llama a error() - Tambi�n limpieza autom�tica
- 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
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
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)
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
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|
�
completeAplicar a m�ltiples Observables
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
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
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()
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 memoriaBuen ejemplo: Llamar tanto a next() como a complete()
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
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 funcionaBuen ejemplo: Regenerar destroy$
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
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)
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
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�n | Ciclo de vida | M�todo de gesti�n |
|---|---|---|
| Estado global | Toda la aplicaci�n | BehaviorSubject + AsyncPipe |
| Espec�fico de p�gina/ruta | Mientras la ruta es v�lida | takeUntil(routeDestroy$) |
| Espec�fico de componente | Mientras existe el componente | takeUntil(destroy$) or AsyncPipe |
| Llamada API �nica | Hasta completar | take(1) or first() |
Mejor pr�ctica 4: Establecer condiciones de finalizaci�n expl�citas
L Mal ejemplo: No est� claro cu�ndo termina
import { fromEvent } from 'rxjs';
fromEvent(document, 'click').subscribe(() => {
console.log('Click');
});Buen ejemplo 1: L�mite de veces
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
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
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.
## 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 navegadorPr�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
- Chapter 2: Ciclo de vida de Observable - Fundamentos de subscribe/unsubscribe
- Chapter 10: Errores comunes y soluciones - Subscribe anidado, fugas de memoria, etc.
- Chapter 13: Patrones de manejo de formularios - Aplicaci�n pr�ctica (en preparaci�n)
- Chapter 8: Depuraci�n de fugas de memoria - M�todos de depuraci�n
<� Ejercicios de pr�ctica
Problema 1: Corregir fuga de memoria
El siguiente c�digo tiene una fuga de memoria. Corr�gelo.
class ChatComponent {
ngOnInit() {
interval(5000).subscribe(() => {
this.chatService.checkNewMessages().subscribe(messages => {
console.log('Nuevos mensajes:', messages);
});
});
}
}Ejemplo de respuesta
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
- A�adir Subject
destroy$ - Detener interval con
takeUntil(this.destroy$) - Resolver subscribe anidado con
switchMap - 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.
- Petici�n HTTP (solo una vez)
- Conexi�n WebSocket (durante la existencia del componente)
- Estado global de usuario (toda la aplicaci�n)
Ejemplo de respuesta
1. Petici�n HTTP (solo una vez)
// 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)
// 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)
// 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