¿Qué es ngOnDestroy y cómo prevenir fugas de memoria?
En el desarrollo de aplicaciones web, comprender cómo manejar correctamente el ciclo de vida de los componentes es crucial, especialmente cuando hablamos de prevenir fugas de memoria. En Angular, el método ngOnDestroy es nuestra ancla para abordar esto efectivamente. Este método se ejecuta cuando un componente se desmonta, liberando los recursos que el componente estaba utilizando. Vamos a desglosar este concepto con un ejemplo práctico y ver cómo evitar que ciertas tareas sigan consumiendo memoria innecesariamente.
¿Cómo implementar un contador reactivo con setInterval?
Para comprender mejor el papel de ngOnDestroy, primero configuraremos un contador simple que actualiza su valor cada segundo. Usaremos setInterval para incrementar este contador y ver cómo todo encaja en nuestro ciclo de vida del componente.
letcounterRef: number |undefined;// Variable para almacenar la referencia del intervalo// Ejemplo de creación de un contador con setIntervalcounterRef =window.setInterval(()=>{this.counter.update(value=> value +1);// Actualizamos el contador},1000);
En este bloque de código, hemos creado un contador que se incrementa cada segundo utilizando setInterval. Esto es útil para entender cómo JavaScript maneja tareas en segundo plano.
¿Por qué setInterval sigue corriendo después de ngOnDestroy?
Un error común es asumir que al destruir un componente con ngOnDestroy, todas las funciones asociadas también se detienen automáticamente. Sin embargo, esto no ocurre de manera predeterminada. En el caso de setInterval, JavaScript seguirá ejecutándolo en el event loop, porque no se le ha dado una instrucción explícita de detención.
ngOnDestroy(){if(counterRef !==undefined){window.clearInterval(counterRef);// Detenemos el intervalo}}
La clave aquí es usar window.clearInterval en ngOnDestroy para matar el intervalo. Al hacerlo, prevenimos que el proceso siga consumiendo recursos innecesarios del navegador, eliminando así cualquier riesgo de fugas de memoria.
¿Qué pasaría con otras tareas en segundo plano?
El principio aplicado a setInterval se extiende a otras ejecuciones en segundo plano, como WebSockets, Subscripciones a Streams de datos, y más. Si no las detienes adecuadamente, pueden continuar ejecutándose incluso después de que el componente haya sido destruido, resultando en un consumo innecesario de memoria y recursos.
Para cada tipo de tarea en segundo plano:
WebSockets: Cerrar el socket explícitamente en ngOnDestroy.
Subscripciones: Utilizar métodos de unsubscribe para cancelar subscripciones activas a data streams.
Promesas/Fetch: Manejar correctamente para evitar operaciones pendientes.
Al entender y aplicar estos principios, puedes crear aplicaciones web que no solo funcionan correctamente, sino que también gestionan recursos de manera eficiente, asegurando una experiencia de usuario más fluida y libre de problemas de memoria. La atención a estos detalles es lo que distingue a un desarrollador experto en Angular. ¡Continúa explorando y aprendiendo para convertirte en uno!
Para los que tienen el error de windows no definido les comento brevemente: Lo que sucede es que tu proyecto esta definido para ser una aplicación que se renderiza desde el servidor (SSR) y en el servidor el elemento windows no existe. Puedes quitar esta opción de tu proyecto eliminando angular universal o puedes indicarle al proyecto que si encuentra el comando windows no las ejecute hasta estar del lado del cliente. Te dejo los comandos que puedes agregar para lograr esto:
Agregar las siguientes dependencias:
import { Component, signal, OnInit, OnDestroy, PLATFORM_ID, Inject } from '@angular/core';import { CommonModule, isPlatformBrowser } from '@angular/common';
Donde tengan el comando window debe estar dentro del siguiente if que lo que hace es preguntar estas del lado del cliente o del servidor. Si estas del lado del cliente ejecuta las lineas de adentro:
if (isPlatformBrowser(this.platformId)) {
// write your client side code here }
En el proyecto se hace en el ngOnInit y en el ngOnDestroy por ejemplo para el OnInit
Bueno voy a hablar de lo mismo que los comentarios de abajo de como solucionar el error.
Descripcion
El codigo de angular se ejecuta solamente del lado del servidor SSR (Server Side Render) por las configuraciones que deberiamos tener en este momento.
Por lo cual no existen (window y document) hasta que pase la primera renderizacion por eso el codigo funciona aun con el mensaje de error en consola.
Solucion
Soluciones hay varias pero usar las herramientas que ofrece Angular es muy util esas son Inject, PLATFORM_ID from @angular/core y la validacion de isPlatformBrowser from @angular/common.
exportclassCounterComponent{ @Input({required:true}) duration: number =0;// usar el hardtypo no es obligatorio @Input({required:true}) message: string =''; counter =signal(0);counterRef: number |undefined;constructor(@Inject(PLATFORM_ID)privateplataformId: object){// Never asyncconsole.log('construnctor');console.log('-'.repeat(10));// before show component }ngAfterViewInit(){// after render // handle child changedconsole.log('ngAfterViewInit');console.log('-'.repeat(10));if(isPlatformBrowser(this.plataformId)){// Esto se va a ejecutar solamente en el navegador// podemos usar todo lo que queramos del DOM dentro del if this.counterRef=window.setInterval(()=>{console.log('run interval');this.counter.update(statePrev=> statePrev +1);},1000)}}}
Esto es muy util si queremos asegurarnos de que ese bloque de codigo se va a ejecutar solamente en el navegador.
Creo que se podria hacer un decorador para que sea mas facil de implementar y menos repetitivo.
Nota: Ese ejercicio tambien lo podemos hacer con NgZone pero sin agregar los window. Feliz dia de estudio :)
✅
💡 – ngOnDestroy previene fugas y bugs silenciosos
Me llevo que una mala limpieza no siempre rompe la app, pero genera consumo innecesario de recursos y bugs difíciles de detectar a largo plazo.
💡 – El problema no es solo setInterval
Me llevo que el mismo principio aplica a WebSockets, suscripciones, streams y timers: todo lo que se crea debe cerrarse explícitamente.
💡 – setInterval debe limpiarse explícitamente
Me llevo que todo setInterval debe tener su clearInterval en ngOnDestroy, o seguirá ejecutándose en el event loop consumiendo memoria.
💡 – Angular NO detiene procesos automáticamente
Me llevo que Angular no cancela por sí solo setInterval, listeners o procesos en segundo plano, incluso si el componente ya no existe.
💡 – ngOnDestroy es el punto de limpieza del componente
Me llevo que ngOnDestroy se ejecuta justo cuando el componente se desmonta, y es el lugar correcto para liberar recursos y evitar fugas de memoria.
Quizas esto les pueda ser de utilidad con respecto al error de window y entender porque ocurre:
# Error 'window is not defined' en Angular Universal (SSR)
Este error, ReferenceError: window is not defined, ocurre porque un componente está intentando acceder al objeto **window**, que solo existe en los navegadores web. El problema se manifiesta cuando el código se ejecuta en el servidor (**Node.js**) como parte del **Renderizado en Servidor (SSR)**.
---
## 🖥️ El Problema: Servidor vs. Navegador
Una aplicación "universal" de Angular ejecuta el mismo código en dos entornos muy diferentes:
* **En el servidor (Node.js):** Se utiliza para pre-renderizar el HTML y enviarlo rápidamente al usuario. Node.js es un entorno de servidor y no tiene una "ventana" de navegador, por lo que no conoce el objeto **window**.
* **En el navegador (Cliente):** Aquí es donde la aplicación se vuelve interactiva después de la carga inicial. En este entorno, el objeto **window** existe y es fundamental.
El error indica que el código está intentando usar **window** durante la fase de ejecución en el servidor, donde no está definido.
---
## 🛡️ La Solución: Proteger el Código del Navegador
La solución es detectar en qué entorno se está ejecutando el código y solo acceder a **window** (y otros objetos del navegador como document o localStorage) cuando se tiene la certeza de estar en un navegador. Angular proporciona una herramienta ideal para esto: la función **isPlatformBrowser**.
### Pasos para Corregir
1. **Importar herramientas:** En tu componente, importa Inject, PLATFORM\_ID desde @angular/core y isPlatformBrowser desde @angular/common.
2. **Inyectar PLATFORM\_ID:** Solicita el identificador de la plataforma en el constructor del componente.
3. **Crear una condición:** Utiliza isPlatformBrowser() para crear un bloque if que proteja cualquier código que dependa de las APIs del navegador.
### Código de Ejemplo Aplicado
// src/app/domains/shared/components/counter/counter.component.tsimport{Component,OnInit,OnDestroy,Inject,PLATFORM\_ID }from'@angular/core';import{ isPlatformBrowser }from'@angular/common';// \<-- 1. Importar@Component({  selector:'app-counter', // ...})exportclassCounterComponentimplementsOnInit,OnDestroy{ private isBrowser:boolean; constructor(@Inject(PLATFORM\_ID)private platformId:Object){// \<-- 2. Inyectar // Guardamos el resultado en una variable para usarlo fácilmente this.isBrowser=isPlatformBrowser(this.platformId); } ngOnInit(){ // ... tu otra lógica ... // 👇 3. Proteger el código que usa 'window' if(this.isBrowser){ // Este código solo se ejecutará en el navegador // Por ejemplo: // window.addEventListener('scroll', this.miFuncion); } } ngOnDestroy(){ // ... tu otra lógica ... // 👇 3. Proteger el código que usa 'window' también en la destrucción if(this.isBrowser){ // Este código solo se ejecutará en el navegador // Por ejemplo: // window.removeEventListener('scroll', this.miFuncion); } } // ... resto de tu componente ...}LaDiferenciaClave: ¿Dónde se Renderiza tu App?Existen dos tipos principales de aplicaciones en Angular:### 1.AplicaciónClásica(Solo en el Navegador-CSR) 🌎
***Cómo funciona:**El servidor envía un `index.html` casi vacío.El navegador descarga el JavaScript de Angular y construye la página desde cero.***Consecuencia:**Como todo el código se ejecuta exclusivamente en el navegador, el objeto **`window`** siempre está disponible.En este tipo de proyectos, nunca te encontrarás con este error.### 2.AplicaciónUniversal(Servidor y Navegador-SSR) 🖥️➡️🌎
***Cómo funciona:**Elservidor(Node.js) ejecuta el código de Angular primero para generar un HTML completo.Este se envía al navegador para una carga visual instantánea.Luego, el JavaScript de Angular se carga y "toma el control"(hidrata) del HTML existente.***Consecuencia:**El código se ejecuta primero en el servidor, donde **`window`** no existe.Por lo tanto, el error **`window is not defined`** siempre sucederá si intentas acceder a las APIs del navegador sin la protección adecuada.## EnResumenLa siguiente tabla resume cuándo esperar este error:Si tu proyecto...¿Ocurrirá el error `window is not defined`?**NO usa SSR**(es una app clásica o CSR)**NUNCA****SÍ usa SSR**(es una app universal)**SIEMPRE**, si no proteges el código específico del navegador.Exportar a Hojas de cálculo
Proteger el código del navegador con **`isPlatformBrowser`** es una práctica estándar y fundamental en el desarrollo de aplicaciones universales con Angular.
para los que estuvieron teniendo esta advertencia me tomo un tiempo pero logre saber que era y una situación del ngZone hice esto en el constructor y lo implemente como una función antes del this.counterRef en el ngOnInt
Ya que la excluye fuera de la zona de Angular
Para el caso del error de windows no está definido, en mi caso lo solucione, agregando una if para el windows.clearInterval, es decir si counterRef tiene un valor (es decir, no es null o undefined), se llama a windows.clearInterval para destruir el objeto.
ngOnDestroy() {
// before destroying the component console.log('ngOnDestroy');
console.log('_'.repeat(10));
console.log('valor de counterRef: ',this.counteRef); if(this.counteRef) {
window.clearInterval(this.counteRef);
}
}
Solucion al error de window, en angular 19
Importar PLATFORM_ID e Inject en angular core
y isPlatformBrowser
import{Component,Input, signal,SimpleChanges,PLATFORM_ID,Inject}from'@angular/core';import{ isPlatformBrowser }from'@angular/common';```en el constructor pasar este parametro
```ts
constructor(@Inject(PLATFORM_ID)private platformId:Object){// no async// before render// una vezconsole.log('constructor');console.log('-'.repeat(10));}```y en los metodos de ngOnInit() y ngOnDestroy() agregar una validacion para detectar si el código está ejecutándose en el cliente o en el servidor
```ts
ngOnInit(){// after render// una vez// async, then, subsconsole.log('ngOnInit');console.log('-'.repeat(10));console.log('duration =>',this.duration);console.log('msg =>',this.msg);if(isPlatformBrowser(this.platformId)){this.counterRef=window.setInterval(()=>{console.log('run interval')this.counter.update(statePrev => statePrev +1);},1000);}}ngOnDestroy(){console.log('ngOnDestroy');console.log('-'.repeat(10));if(isPlatformBrowser(this.platformId)){window.clearInterval(this.counterRef);}}``` 
Hay un curso de angular como primeros pasos? :D es que siento que este curso es como para personas que ya han trabajdo con angular, si. entiendo la clase pero digamos que no como cuando ves el curso de react con Juan que es de 0 al nivel que quieras llegar... Igual bueno podria buscar documentacion ::)
Me sale este error acerca de do.Something :(
TS2339: Property 'doSomething' does not exist on type 'CounterComponent'. [plugin angular-compiler]
Me sale el NG0505 y no encontré una solución, vi los que pusieron mas abajo, pero no me resulto, para empezar yo tengo "@angular-devkit/build-angular:application" y no browser y la segunda donde tengo que cambiar los booleanos en development tampoco resulto :(
y me sale esto de error
entro a ver el error y me estoy aca, pero no se que hacer o si realmente ese es el problema
a mi me dio error cuando hice el this.counter = window.setInterval(()
El tipo 'number' no se puede asignar al tipo 'WritableSignal<number>'.ts(2322)