Señales (Signals)
¿Qué es ZoneJS?
Zone.js es una librería fundamental en Angular que actúa como un observador global de las aplicaciones. Su principal función es capturar y monitorear cualquier actividad asíncrona que ocurra dentro de una aplicación, ya sean eventos del DOM, promesas, timeouts, o cualquier otro tipo de operación que no se ejecute de manera inmediata.
¿Por qué es tan importante Zone.js en Angular?
- Detección de Cambios: Gracias a Zone.js, Angular puede detectar de manera eficiente cuándo se producen cambios en los datos de una aplicación. Esto es crucial para actualizar la vista de forma reactiva y mantenerla sincronizada con el modelo de datos.
- Simplificación de la Gestión de Asincronía: Zone.js abstrae la complejidad de manejar múltiples tareas asíncronas en JavaScript, permitiendo a los desarrolladores centrarse en la lógica de su aplicación.
- Integración con Angular: Zone.js está estrechamente integrado con Angular y es un componente esencial de su arquitectura. Facilita la implementación de características como el cambio de detección y la inyección de dependencias.
Zone.js crea un entorno de ejecución especial llamado "zona" para cada componente de Angular. Cuando ocurre un evento o una operación asíncrona dentro de una zona, Zone.js lo detecta y puede ejecutar tareas adicionales, como marcar la zona (el componente y todos sus antecesores en el árbol) como "sucia" (dirty) para indicar que la vista necesita ser actualizada.
Estrategias de detección de cambios
Para la detección de cambios, Angular utiliza ZoneJS en mayor o menor medida a la hora de saber qué componentes (vistas) debe actualizar y qué cambios debe vigilar. Cuando se utiliza onPush + signals, o si directamente se configura la aplicación como zoneless (sin ZoneJS), Angular es capaz de saber directamente qué vistas debe actualizar de forma mucho más eficiente.
Angular comprueba en cada ciclo de detección de cambios si alguna propiedad de un componente vinculada en la plantilla ha cambiado de estado. Los eventos que pueden desdencadenar una detección de cambios son los siguientes:
- Eventos del DOM
- Peticiones Http
- Temporizadores (setTimeout, setInterval)
- Otras APIs asíncronas
- Código ejecutado dentro de la zona de Angular utilizando NgZone.run
Si queremos evitar que Angular active la detección cambios de forma innecesaria en alguna tarea, con el objetivo de mejorar el rendimiento, podemos inyectar el servicio NgZone en el componente y ejecutar el código dentro del método runOutsideAngular. Esto es útil cuando queremos por ejemplo, trabajar con librerías ajenas a Angular y cuyos efectos sobre el DOM (eventos, timers, peticiones HTTP) no afectan nuestro código.
A veces puede pasar lo contrario. Si utilizamos por ejemplo una librería de JavaScript que no se integra con Angular, esta se inicializa fuera de la zona de Angular (no está controlada por ZoneJS), por lo que los cambios producidos por eventos de dicha librería no son detectados por Angular y no actualiza la vista hasta el siguiente ciclo de detección (por ejemplo cuando se produzca un evento de click).
Por defecto (default)
Por defecto, Angular utiliza una estrategia bastante agresiva, ya que cuando detecta algún cambio, comprueba los valores asociados a la plantilla de todos los componentes de la aplicación. Esto en aplicaciones simples no supone una gran pérdida de rendimiento y puede facilitar la lógica de las mismas. Sin embargo, puede llevarnos también a utilizar malas prácticas en el desarrollo de código al no preocuparnos sobre la detección de cambios y sus consecuencias. Esto se conoce como Zone Pollution.

onPush
La estrategia onPush hace que Angular ejecute la detección de cambios en ese componente de forma mucho más limitada. En este caso, comprueba los cambios para marcar el componente como sucio solo cuando:
- El componente recibe nuevos valores a través de sus parámetros de entrada (@Input). Angular comprueba, en el caso de usar esta estrategia, si el valor ha cambiado utilizando ==, por lo que si recibimos un objeto o array, debe llegar una nueva referencia, no valen modificaciones internas.
- Angular gestiona un evento en el componente en cuestión, vinculado desde la plantilla, o en alguno de sus descendientes.
- Se marca el componente manualmente como dirty.
Esta estrategia también afecta si se produce un cambio fuera del subárbol del componente actual. Es decir, si Angular marca como dirty un componente que no es descendiente del actual, Angular no comprobará cambios en este componente si no ha sido marcado como dirty ni en ninguno de sus descencientes, ganando así en eficiencia.

Vamos a ver que pasaría si utilizamos esta estrategia en el componente ProductsPageComponent. Por ejemplo, si cambiamos el valor de la propiedad showImage con un setTimeout en el constructor, veremos que no tiene efecto hasta que haya otro evento que genere una detección de cambios (foco en un campo del formulario por ejemplo, ya que ngForm y ngModel son descendientes y escuchan estos eventos). Lo mismo pasa al cargar la imagen en base64, ya que no es un evento directamente producido por la plantilla.
Si queremos provocar que Angular detecte cambios, podemos marcar el componente como dirty utilizando el método markForCheck del servicio ChangeDetectorRef. Por ejemplo, para detectar que la imagen a previsualizar ha cambiado:
En el caso de que los cambios se produzcan fuera de la zona de Angular (una librería externa por ejemplo), en lugar de llamar a NgZone.run, podemos marcar el componente como dirty y forzar una detección de cambios inmediata (síncrona) con el método detectChanges() del objeto ChangeDetectorRed. Esto hay que usarlo con cuidado, y se debe evitar en la medida de lo posible porque puede afectar al rendimiento negativamente (mejor usar NgZone.run combinado con markForCheck).
onPush + signals
En el caso de usar signals y vincularlas en la plantilla del componente, si lo combinamos con la estrategia OnPush, Angular solo marcará el componente actual como dirty, y ejecutará la detección de cambios solo en el componente actual y desde el componente raíz (AppComponent) solo se propagará por aquellos componentes que no tengan la estrategia OnPush.

Para comprobar si tu aplicación está preparada para funcionar sin ZoneJS (zoneless) todos tus componentes deberían utilizar la estrategia OnPush y gestionar los cambios como se ha explicado con dicha estrategia, o mediante signals referenciadas en plantilla.
Zoneless
Desde la versión 18 de Angular, existe la posibilidad de crear una aplicación zoneless, es decir, prescindiendo completamente de ZoneJS. Esto está en una fase aún experimental. El funcionamiento es muy similar a la estrategia OnPush, pero quitando la dependencia de ZoneJS.
Para activar esta característica, debemos sustituir la llamada a <b>provideZoneChangeDetection en el array providers del archivo app.config.ts por la llamada a la función provideExperimentalZonelessChangeDetection.
Después, podemos borrar la dependencia de la librería del archivo angular.json, buscando los arrays polyfills (build y test) y borrando las dependencias de la librería.
Finalmente, ya podríamos desinstalar zonejs: npm r zone.js
Cuidado: Hasta que la detección de cambios sin ZoneJS deje de ser experimental, esta puede estar sujeta a cambios y es posible que algunas librerías de Angular no tengan un soporte adecuado ya que es algo muy reciente.
Si quieres comprobar como funciona la detección de cambios en diferentes escenarios, puedes probar con esta demo. Después de cambiar algo, debes hacer click en el mensaje superior para provocar una detección de cambios y ver qué componentes se ven afectados.
Signals API
La API de Angular signals (señales) se introdujo en la versión 16 (experimental) y para la versión 19, gran parte de la API será considerada estable (no sujeta a cambios significativos).
Una señal es un objeto que almacena un valor, y cada vez que ese valor se modifica, se notifica a aquellas partes del código que utilizan el valor de la señal (plantilla de un componente) o funciones programadas para ejecutarse automáticamente cuando el valor cambia.
Creación de una señal: signal
Para crear una señal llamaremos a la función signal (@angular/core) pasándole el valor inicial. Esto nos devolverá un objeto del tipo WritableSignal, lo que implica que su valor puede modificarse en cualquier momento.
Acceder al valor de una señal
Las señales funcionan también como getters del valor que almacenan. Esto quiere decir que para acceder a su valor, basta con añadir los paréntesis como si llamaramos a una función. Esto también ocurre en la plantilla del componente.
Cambiar el valor de una señal
Para cambiar el valor de una señal tenemos 2 métodos. El primero es set, al que le pasaremos directamente el nuevo valor que almacenaremos.
También podemos utilizar el método update, que permite cambiar el valor mediante una función que recibe el valor actual. Lo que devolvemos en esta función sería el nuevo valor almacenado (similar al método map de los arrays).
En ambos casos, si el valor almacenado es un array u objeto, siempre debemos devolver una nueva referencia para que Angular detecte que el valor ha cambiado. Es decir, un nuevo array u objeto a partir del original.
¿Debo cambiar siempre la referencia?
Si no cambias la referencia a la que apunta la señal, Angular no detecta que el valor de la señal ha cambiado (similar a la estrategia OnPush con los inputs del componente). Esto hará que las señales que dependan de esta (usando computed por ejemplo) no actualicen su valor.
Para la detección de cambios en la plantilla dependerá del origen del cambio. Como sabemos de la estrategia OnPush o zoneless, si el cambio procede de un evento generado por el componente, Angular detectará que debe comprobar si alguna propiedad vinculada en la plantilla ha cambiado y actualizarla. En caso de que el cambio venga de otra fuente (una operación asíncrona o un evento de una librería), entonces no detectará que hay un cambio si no cambiamos la referencia de la señal en el componente.
Para indicarle a Angular que debe revisar las propiedades vinculadas a la plantilla porque ha habiado algun cambio, siempre puedes llamar de forma manual al método ChangeDetectorRef.markForCheck() para indicarle a Angular manualmente que debe marcar el componente como dirty y comprobar los cambios. Ejemplo de cambio a una propiedad interna: producto().rating = 4.
Si quieres salir de dudas, con el componente utilizando la estrategia OnPush (o en una aplicación zoneless), prueba a ver si se detectan los cambios y se actualiza la plantilla sin necesidad de llamar al método sugerido antes.
¿Cuando usar señales?
Aunque utilicemos la estrategia OnPush o una aplicación Zoneless, en principio no hace falta usar señales para todos los valores, ya que Angular es capaz de detectar cambios en valores normales vinculados a la plantilla. Sin embargo, pueden ser muy útiles en ciertos casos:
- Cuando el valor se cambia desde una operación asíncrona (evento, observable, etc) y queremos que detecte el cambio.
- El punto de arriba es independiente de si la operación se hace en el componente o desde una librería ajena a Angular, ya que las señales no dependen de ZoneJS
- Cuando el valor se obtiene desde otro lugar (un servicio por ejemplo)
- Cuando queremos reaccionar al cambio de valor. Esto se hace, por ejemplo, actualizando los valores de una señal a partir de otra (computed, linkedSignal) o ejecutando alguna tarea (effect)
- Operaciones más avanzadas (utilizando un paradigma de programación más reactivo) combinando las señales con observables mediante las funciones toSignal y toObservable.
Los cambios generados a partir de manejadores de eventos ejectuados desde la plantilla o cambios en valores asociados a formularios (ngModel), marcan el componente como dirty para su comprobación, aunque el valor no esté en una señal. En estos casos Angular actualizará la plantilla del componente automáticamente.
Señales computadas: computed
Se pueden crear señales cuyos valores dependen de los valores de otras señales, y que cambian dinámicamente cuando los valores de las señales de las cual dependen cambian a su vez.
Para crear una señal dependiente de otras usamos la función computed. Esta recibe una función por parámetro, y lo que devolvamos ahí será el valor que almacenará la señal computada. Si utilizamos valores de otras señales en esta función, cuando estas cambién, la función se ejecutará otra vez actualizando el valor de la señal dependiente.
Las señales generadas a partir de la llamada a computed son de solo lectura. Es decir, están tipadas como Signal en contraposición a WritableSignal de las señales normales.
Evitar detección innecesaria (untracked)
Las señales utilizan el patrón Observer para gestionar las dependencias. En la práctica, al llamar a la señal para leer su valor, tanto en la plantilla como en funciones tipo computed o effect. se genera una subscripción a los cambios de dicha señal. Cuando el valor de la misma cambia, se notifica a las funciones dependientes, o al componente en cuya plantilla se accede al valor.
Esto significa que por ejemplo, en una función computed o effect, si accedemos al valor de varias señales, el cambio en cualquiera de ellas notificaría para que se volviese a calcular/ejecutar. Puede ocurrir también, que el valor de una señal se lea varias veces (varias subscripciones) o que llamemos a un método que indirectamente lea el valor de otra señal (se genera una subscripción también a dicha señal).
Si queremos que alguna señal, o código que pueda acceder a otras señales, no genere dependencias. Es decir, que no se recalcule/ejecute la función otra vez, lo podemos meter dentro de la función untracked. Si devolvemos algo dentro de esta función, a su vez es devuelto por la misma hacia fuera.
Se considera una buena práctica guardar los valores en constantes de aquellas señales de las cuales queremos depender, y ejecutar el resto del código dentro de una función untracked. Esto no es necesario en ejemplos tan simples como el anterior, y se suele utilizar más en funciones tipo effect que veremos a continuación.
Señales mixtas: linkedSignal
En Angular 19 se introduce la función linkedSignal para crear señales. Es similar a la función computed pero devuelve una señal editable (WriteSignal). Esto permite cambiar su valor en cualquier momento, pero en el momento en que cambie una señal de la cual dependa, su valor será sobrescrito en base a lo que devuelva la función predefinida.
Efectos: effect
Las funciones effect tienen sentido cuando dependen de alguna señal y sirven para ejecutar determinadas tareas en el momento en que el valor de alguna de ellas cambia. Al contrario que computed no devuelven ningún valor por lo que no se pueden utilizar para asignar valores dependientes de otras señales. En cualquier caso, siempre se ejecutan al menos la primera vez.
Este tipo de funciones se suelen crear en el constructor, ya que necesitan acceder al contexto de inyección de dependencias y una vez construido el objeto este ya no está accesible.
Si se necesitan crear más tarde por alguna razón, se les puede pasar el contexto de inyección de dependencias manualmente, obteniendo una referencia con inject.
El uso recomendado de la función effect es el siguiente:
- Hacer log de los datos y cuando cambian por motivos de depuración etc.
- Sincronizar datos con localStorage
- Manipular el DOM fuera de la automatización que ofrece Angular (por ejemplo, librerías JavaScript, etc.)
Aunque se aconseja evitarlo cuando sea posible, se pueden modificar señales dentro de la función effect. Esto se hace para evitar problemas como actualización circular de dependencias (bucle infinito donde se actualiza alguna señal que a su vez hace que se vuelva a ejecutar la función effect).
Por ello hay que tener varias cosas importantes en cuenta:
- Siempre que se pueda, utilizar funciones alternativas como computed, o nuevas funciones de Angular 19 como linkedSignal o resource/rxResource, además de las funciones para operar con observables como toSignal y to Observable.
- En algunos casos, o mientras se va aprendiendo lo mencionado arriba, no hay problema en cambiar señales dentro de effect con cuidado.
- Es importante el uso de la función untracked. De esa manera no se generan accidentalmente dependencias innecesarias que pueden meternos en un bucle circular.
En Angular 17 y 18, se necesita la opción allowSignalWrites para permitir modificar una señal dentro de effect. En Angular 19 ya no es necesaria.
Avanzado: Cuanto más conocimiento del framework adquieras, más podrás optimizar tu código. En el futuro, podríamos utilizar los operadores de interconexión entre señales y observables (toObservable y toSignal) para prescindir de la función effect.