Artículo original: Signals in Angular – How to Write More Reactive Code
¡Una nueva y emocionante característica llega a Angular! Las señales, estas proveen una nueva forma de comunicación de nuestro código que comunique a nuestras plantillas que los datos han cambiado. Esto mejora la capacidad de detección de cambios de Angular, lo que también mejora el rendimiento y hace nuestro código más reactivo.
Ya puedes probar las señales, están disponibles con acceso anticipado para desarrolladores (developer preview) en Angular v16, que se publicará en mayo de 2023. Te muestro como hacerlo más adelante.
Si gustas puedes ver el vídeo asociado (en inglés):
Encuentra el código de este tutorial aquí: https://stackblitz.com/edit/angular-signals-deborahk.
Antes de adentrarnos en detalles del "qué" y el "cómo", vamos a empezar con el "por qué". Entonces, ¿por qué usar las señales?
¿Por qué necesitamos las señales?
Empecemos con un ejemplo simple que no utiliza señales. Digamos que estás escribiendo código para realizar algunas operaciones matemáticas.
let x = 5;
let y = 3;
let z = x + y;
console.log(z);//~8
¿Qué es lo que este código imprime por consola? Así es, imprime 8
.
En alguna parte más adelante en el código cambiamos el valor de x
. ¿Qué valor de z
se imprime por consola ahora?
let x = 5;
let y = 3;
let z = x + y;
console.log(z);
x = 10;
console.log(z);//~8
¡Aún imprime 8
! Esto se debe a que el valor se asigna a z
desde el inicio cuando la expresión se ha evaluado. La variable z
no reacciona a los cambios en x
o en y
.
¡Pero queremos que nuestras variables reaccionen a los cambios!
Una de las razones por las que usamos Angular es para crear sitios web interactivos y reactivos, es decir, con capacidad de reaccionar a los cambios de los datos.
Como por ejemplo en la Figura 1. Cuando el usuario edita la cantidad, las variables relacionadas, como el total, sub-total e impuestos, deberían reaccionar y ajustar los costos automáticamente. Si el usuario decide borrar un item del carrito también queremos que las variables relacionadas sean capaces de re-calcular los costos.

Con las señales nuestro código puede ser más reactivo. En nuestro anterior ejemplo la implementación de señales se vería así:
const x = signal(5);
const y = signal(3);
const z = computed(() => x() + y());
console.log(z()); // 8
x.set(10);
console.log(z()); // 13
Veremos la sintaxis y lo demostraremos a detalle más adelante. Por ahora, el código de arriba define dos señales: x
y y
además de darles valores iniciales de 5
y 3
. Entonces definimos una señal computada z
, que es la suma de x
y y
. Debido a que las señales proveen notificaciones de cambios cuando cambian los valores de las señales x
o y
, cualquier valor computado a partir de esas señales se re-calculará automáticamente. ¡Este código es reactivo! ¡Excelente!
Las señales computadas reaccionan y se re-calculan cuando cualquiera de sus señales dependientes cambia. Si una señal está vinculada en una plantilla de Angular, al cambiar dicha señal, el mecanismo de detección de cambios de Angular actualiza cualquier vista que pueda leer la señal. Y los usuarios visualizan los valores modificados.
Por lo que es posible dar respuesta a la pregunta de, ¿por qué necesitamos las señales? De la siguiente manera:
- Las señales permiten la reactividad.
- Usando señales podemos tener mayor control en la detección de cambios, lo cual puede mejorar el desempeño.
Profundicemos un poco más para entender lo que es una señal y cómo se usa.
¿Qué es una señal?
Podemos pensar de una señal como un valor que además tiene la capacidad de notificar cambios. Una señal es un tipo especial de variable que almacena un valor. Pero a diferencia de otras variables, una señal también provee notificaciones cuando el valor de la variable cambia.
Imagínate una variable normal como si fuera un estante o repisa, como en el lado izquierdo de la figura 2. Cuando un valor se asigna a la variable, ésta se colocaría en el estante. Cualquier código dentro del alcance puede simplemente ver (leer) la variable en el estante.
Una señal tiene mayor parecido a una caja, como se muestra al lado derecho de la figura 2. Al crear una señal, creamos algo análogo a una caja y el valor se coloca dentro de ésta. Cuando el valor que se encuentra dentro cambia, la caja emite un brillo para avisarle. Para leer la señal, es decir el valor almacenado, primero debemos abrir "la caja" usando paréntesis: x()
. Técnicamente llamamos a la función "getter" (literal de conseguidor, método o función que devuelve un valor) de la señal para leer el valor de esta.
Ahora pues, ya sabemos que es una señal.
- Una señal es una variable con notificaciones de cambios.
- Es un valor reactivo o bien un "reactivo primitivo".
- Siempre tiene un valor
- Es síncrona.
- No es un sustituto o remplazo de RxJS ni de Observables para operaciones asíncronas como
http.get
.
¿Dónde podemos usarlas?
- En componentes para monitorear su estado local.
- En directivas.
- En servicios para compartir el estado entre componentes.
- Para su lectura en una plantilla (del inglés template) para mostrar lo valores.
- O en cualquier otra parte de tu código.
A continuación, vayamos paso a paso viendo cómo crear y usar señales.
Cómo crear una señal
Para usar una señal tenemos primero, por supuesto, que crearla.
cantidad = signal<number>(1);
Esta es la sintaxis para escribir el código para crear e inicializar una señal usando el método constructor signal()
.
De forma opcional como podemos ver, es posible pasar un parámetro genérico para definir el tipo de dato de la señal. Una señal puede ser de tipo string, number, array, object o cualquier tipo de dato. En muchos casos el tipo de dato puede inferirse y por lo tanto no es necesario u obligatorio pasar el parámetro de tipo genérico.
Pasemos al constructor el valor por defecto para nuestra señal, ya que una señal siempre debe tener un valor, y por lo tanto, es necesario inicializarlas con un valor por defecto.
Aquí te muestro algunos otros ejemplos:
cantidad = signal(1);
cantidadDisponible = signal([1, 2, 3, 4, 5, 6]);
vehiculoSeleccionado = signal<Vehiculo>({
id: 1,
nombre: 'AT-AT',
precio: 19416.13
});
vehiculos = signal<Vehiculo[]>([]);
La primera línea de este código crea una señal numérica con un valor por defecto (predefinido) de 1
. Ya que el valor predeterminado es de tipo number
, la cantidad
es una señal que contiene un número. El parámetro del tipo genérico no se necesita pasar, ya que como vimos el tipo del dato es inferido automáticamente.
La segunda línea es una señal que contiene un arreglo de números, donde su valor predefinido corresponde con los valores del 1 al 6. De nuevo, no necesitamos pasar el parámetro de tipo genérico ya que el tipo de dato se infiere automáticamente a partir del tipo de dato del valor que hemos pasado.
La señal vehiculoSeleccionado
contiene un objeto de tipo Vehiculo
. En este ejemplo el tipo no puede inferirse, por lo que especificamos un parámetro de tipo genérico para Vehiculo
.
La señal vehiculos
contiene un arreglo para objetos de tipo Vehiculo
, que está vacío por defecto. Para que este arreglo posea tipos estático y pueda recibir elementos del tipo de dato Vehiculo
, agregamos el parámetro genérico de tipo de datos <Vehiculo[]>
.
Una señal que fue creada utilizando el constructor es editable, por lo que puedes asignarle un valor nuevo o diferente, actualizarla en base a su valor actual o mutar su contenido. En breve veremos ejemplos de cómo realizar estas operaciones.
Una vez que hayas creado una señal es posible que quieras leer su valor.
Cómo leer una señal
Anteriormente planteamos pensar que una señal es una caja. Hablando metafóricamente podemos decir que para leer el valor de una señal es necesario primero abrir la caja. Ya que las señales son funciones, esto lo hacemos invocando la señal con el uso de paréntesis.
quantity();
Empecemos con el nombre de la señal y enseguida con el paréntesis de apertura y luego el de cierre. Técnicamente esto a su vez invoca a la función "getter", la cual es creada "por debajo", por lo que no es visible en el código.
Cuando se trabaja con Angular es común leer las señales en el "template" (plantilla).
<select
[ngModel]="cantidad()"
(change)="alSeleccionarCantidad($any($event.target).value)">
<option *ngFor="let c of cantidadDisponible()">{{ c }}</option>
</select>
<div>Vehículo: {{ vehiculoSeleccionado().nombre }}</div>
<div>Price: {{ vehiculoSeleccionado().precio }}</div>
<div [style.color]="color()">Total: {{ precioTotal() }}</div>
La plantilla de arriba muestra una caja de selección de cantidad. La directiva [ngModel]
lee el valor de la señal cantidad
y crea un enlace o "binding" a ese valor.
El enlace o "binding" al evento change
invoca el método alSeleccionarCantidad()
del componente.
El elemento html option
usa la directiva ngFor
para iterar por cada elemento del arreglo en la señal cantidadDisponible
. Lee la señal y crea una opción en el select
para cada elemento del arreglo.
Debajo del elemento select
encontrarás tres elementos div
, el primero lee la señal vehiculoSeleccionado
, luego accede a su propiedad nombre
. El segundo elemento div
lee la señal vehiculoSeleccionado
y muestra su propiedad precio
. El último div
lee la señal precioTotal
(que no hemos definido aún), y utiliza el valor de la señal color
(que tampoco hemos definido todavía) para determinar el color del texto.
Es importante nota que al leer una señal siempre obtendremos el valor actual, y el código no tiene conocimiento de ningún valor previo.
Cuando el usuario elige una cantidad diferente en el elemento select
queremos cambiar el valor de la señal cantidad
. De esa manera la señal cantidad
se transforma en la "fuente de verdad" para la cantidad seleccionada por el usuario. Veamos cómo hacerlo a continuación.
Cómo cambiar el valor de una señal
El método set
reemplaza el valor de una señal con un nuevo valor. Básicamente "abre la caja", quita el ítem actual y asigna un nuevo ítem que tomará el lugar del anterior.
this.cantidad.set(cant);
Una situación común es cambiar el valor de la señal en base a una acción del usuario. Por ejemplo:
- El usuario selecciona una nueva cantidad usando el elemento
select
. - El evento ligado al elemento
select
invoca al métodoalSeleccionarCantidad()
y pasa la cantidad seleccionada por el usuario. - La acción del usuario se maneja en el componente usando la función o método definido para este propósito.
- El nuevo valor se asigna a la señal
cantidad
.
Este es un ejemplo de una función para el manejo de eventos (event handler):
alSeleccionarCantidad(cant: number) {
this.cantidad.set(cant);
}
En el momento en que se haya asignado o re-asignado la señal, el código notifica a cualquier consumidor de la señal que esta ha cambiado. En este contexto, un consumidor es cualquier código que tenga interés en recibir notificaciones de cambios del valor de la señal.
¿Cómo es que el consumidor indica que tiene interés en ser notificado de los cambios de una señal en particular?
Si un código lee una señal, ese código será notificado cuando la señal cambie.
Si una plantilla lee una señal, será notificada cuando la señal cambie y la "vista" (view) sea programada para volver a renderizarse.
Por lo que el acto de leer una señal registra el interés del consumidor de observar el valor y los cambios de dicha señal. El equipo de Angular le llama a esto la regla de oro de las señales en componentes: "la detección de cambios en un componente se agendará cuando y sólo cuando la lectura de una señal en el plantilla notifique a Angular que su valor a cambiado".
Aquí un ejemplo para ilustrar el proceso. Digamos que hay algo de trabajo realizándose dentro del método (mostrado abajo) que busca ajustar la cantidad. Digamos que quizá el caso de uso podría ser algo como una promoción, que si por ejemplo el usuario elige una cantidad igual o mayor a 5 entonces se lleva uno gratis. El punto es que la señal cantidad
podría cambiar en varias ocasiones durante la ejecución del método.
alSeleccionarCantidad(cant: number) {
this.cantidad.set(qty);
this.cantidad.set(5);
this.cantidad.set(42);
}
La cantidad se muestra en la plantilla usando la vinculación de Angular (binding) como se muestra abajo. Ya que el vínculo "lee" la señal cantidad
, la plantilla "registra" su interés en recibir notificaciones de cambios.
//Angular binding (vinculación en la plantilla)
{{ cantidad() }}
Cuando el usuario elige la cantidad el método alSeleccionarCantidad()
se ejecuta. El código en este método primero asigna el valor seleccionado por el usuario a la señal cantidad
. Luego cuando la nueva señal se asigna esta genera una notificación. En este punto, se agenda la ejecución del mecanismo de detección de cambios de Angular, pero no tiene la oportunidad de hacerlo sino hasta después de la ejecución del método alSeleccionarCantidad()
.
Entonces, el método alSeleccionarCantidad()
continúa asignando el número 5
a la señal, que genera otra notificación de cambio. De nuevo el mecanismo de detección de cambios de Angular es llamado a ejecutarse pero aún debe esperar a que el método alSeleccionarCantidad()
termine su ejecución. El método entonces asigna el valor 42
a la señal y el proceso se repite.
Cuando se completa la ejecución del método alSeleccionarCantidad()
finalmente será turno de ejecución del mecanismo de detección de cambios de Angular. La plantilla lee la señal y obtiene su valor, que es 42
. La plantilla no es "consciente" de ninguno de los valores anteriores. La vista se vuelve a "renderizar" y el nuevo valor de la señal cantidad
se mostrará.
Si la señal cambia, cualquier "consumidor" interesado en su lectura será notificado. Pero el consumidor no recibirá el nuevo valor, sino que, hasta la próxima vez, cuando sea el turno de ejecución del consumidor es entonces que podrá "leer" el valor actual de la señal.
Si conoces cómo funcionan los observables en RxJS podrás notar que las señales pueden ser distintas ya que no emiten valores cómo lo hacen los observables, además de que no requieren una suscripción.
Además del método set()
, también hay otras 2 maneras de cambiar una señal: update()
y mutate()
. El método set()
reemplaza el valor de una señal con un valor nuevo, metafóricamente remplazado el contenido del "cajón" de la señal. Pásale el nuevo valor al método set()
.
// Remplaza el valor
this.cantidad.set(cant);
El método update()
actualiza la señal en base a su valor actual. Pasa una función de "flecha" al método update()
, esta provee el valor actual de la señal para que sea posible cambiarlo programáticamente según sea necesario. En el siguiente código la cantidad se duplica.
// Actualizar el valor de la señal en base al valor actual
this.cantidad.update(cant => cant * 2);
El método mutate()
modifica el contenido del valor de una señal, no el valor de la señal en sí. Úsalo con arreglos para modificar sus elementos y con objetos para modificar sus propiedades. En la siguiente línea de código se incrementa 20% el precio de un vehículo.
this.vehiculoSeleccionado.mutate(v => v.precio = v.precio + (v.precio * .20));
Independientemente de la forma en cómo se modifica la señal, los consumidores serán notificados que ha sucedido un cambio, ellos pueden entonces leer el nuevo valor de la señal cuando es su turno de ejecución.
Cómo definir una señal computada
Frecuentemente tenemos variables en nuestro código que dependen de otras variables, por ejemplo, el precio total de un ítem es su precio unitario por el número o cantidad de ítems deseados de este tipo. Si el usuario cambia la cantidad queremos también cambiar el precio total, para lo que utilizamos señales computadas (computed signals).
Define una señal computada llamando la función computed()
de creación de señales computadas, esta crea una nueva señal que depende de otras. Ahora pasemos a la función computed()
una función que lleve a cabo las operaciones deseadas, esta tendrá acceso a la lectura de el valor de una o más señales para llevar a cabo su computo.
precioTotal = computed(() => this.vehiculoSeleccionado().precio * this.cantidad());
color = computed(() => this.precioTotal() > 50000 ? 'verde' : 'azul');
La primera línea de código de arriba define una señal computada precioTotal
al llamar la función de creación computed()
, la función que pasamos a esta función lee las señales vehiculoSeleccionado
y cantidad
, si cualquiera de estas cambia, la señal computada será notificada y leerá el nuevo valor la próxima vez que se ejecute.
La segunda línea define la señal computada color
, que le asigna el valor verde
o azul
dependiendo del valor de la señal precioTotal
. La plantilla puede vincular a esta señal para mostrar el estilo apropiado.
Una señal computada es sólo lectura y no puede ser modificada con los métodos set()
, update()
ni mutate()
, además su valor se re-evalúa o re-computa cuando:
- Uno o más de sus señales dependientes ha cambiado.
- Y el valor de la señal computada ha sido leído.
La señal computada se "memoiza", lo que quiere decir que guarda el resultado evaluado, este es re-utilizado la próxima vez que es leído.
Digamos por ejemplo que tenemos lo siguiente en nuestra plantilla:
Precio extendido: {{ precioTotal() }}
Precio total: {{ precioTotal() }}
Cantidad a pagar: {{ precioTotal() }}
La primera vez que la plantilla lea la señal computada precioTotal
, el valor se calcula y se guarda en memoria, las siguientes 2 veces que se lee, se re-utiliza el valor almacenado anteriormente. Dicho valor no se re-calcula a menos que alguna de sus señales dependientes haya cambiado.
Cómo usar un efecto
Podría haber momentos en que necesites ejecutar código en respuesta al cambio de alguna señal y que este código tenga efectos secundarios, por ello quiero decir que este código haga un llamado a alguna API o que realice alguna otra operación no relacionada a la señal, en estos casos debemos usar effect()
.
Por ejemplo, digamos que quieres inspeccionar tu código en busca de errores (debug) y para ello deseas imprimir el valor de la señal usando console.log
cada vez que esta sufra un cambio, como sabemos llamar console.log
es un efecto secundario.
Para definir un efecto debemos llamar la función creacional effect()
. Pasémosle a esta función la operación a realizar, la cual será ejecutada nuevamente cada vez que el código reaccione a un cambio en cualquiera de la señales dependientes.
effect(() => console.log(this.vehiculoSeleccionado()));
La función effect()
puede ser invocada desde otra función, pero frecuentemente es llamada desde el constructor u otro código de inicialización, dado que a su vez prepara un tipo de función manejadora (handler).
Alternativamente un efecto puede definirse de forma declarativa como se muestra a continuación:
e = effect(() => console.log(this.vehiculoSeleccionado()));
Un efecto no debería cambiar el valor de ninguna señal, sin embargo si necesitas cambiar una señal en base a un cambio a otra señal dependiente es recomendable que utilices una señal computada en su lugar.
Cómo verás, no es muy común utilizar los efectos, aunque son útiles para imprimir valores por consola o para llamar APIs externas, pero no los uses para trabajar con RxJS ni Observables; habrá funcionalidades de las señales que serán útiles para convertir a y desde los observables.
Cuándo usar señales
Algunas sugerencias.
En primer lugar, continúa utilizando "handlers" en tus componentes como has venido haciendo hasta ahora para manejar o responder a las acciones de los usuarios. Acciones como realizar una selección de una lista desplegable, hacer clic en un botón o ingresar datos en una "caja de texto".
Usa una señal o señal computada en un componente para cualquier estado que pueda cambiar. En este contexto, estado se refiere a cualquier dato que sea administrado por el componente. Cualquier cosas desde una bander estaCargandose
hasta la página actualmente mostrada con los datos seleccionados por el usuario podrían ser señales. Las señales son especialmente útiles cuando se muestran los datos en la plantilla que deben reaccionar a cambios dependientes de otras acciones.
Las señales compartidas deberían ser servicios. Digamos que tenemos un arreglo con los vehículos devueltos que se comparte entre componentes, este podría ser un servicio.
Continúa usando los observables para operaciones asíncronas como las llamadas a http.get()
, cabe mencionar que hay otras funcionalidades que vendrán con las señales para equiparar más fielmente con los observables.
Concluyendo
Las señales representan un avance mayúsculo en las capacidades de programación reactiva y detección de cambios de Angular. Este tutorial respondió a las preguntas: "¿Por qué?", "¿Qué?" y "¿Cómo?" así como "¿Dónde?" y "¿Cuándo?" también.
Las señales están disponibles en vista previa para desarrolladores en Angular v16, como parte de esta vista previa las señales se incluyen en el modelo de detección de cambios. Se espera que futuras capacidades de las señales mejorarán la detección de cambios y marcarán los componentes para revisión de una forma similar como se lleva a cabo actualmente al usar OnPush
con la pipa asíncrona (async pipe).
Una forma sencilla y fácil de probar las señales es usar Stackblitz que es un editor de código en línea que trabaja muy bien con Angular y no requiere instalaciones. Para usar Stackblitz con señales:
- Navega al sitio de Stackblitz: www.stackblitz.com.
- Haz clic en el icono de Angular para crear un proyecto Angular.
- Edita el archivo
package.json
resultante y cambia las versiones de los paquetes de @angular a sus versiones más actuales "pre-realease" (Angular v16). - Guarda el proyecto para que se actualicen las dependencias.
- Dale una probada a las señales.
Para ver estos pasos en acción dale un vistazo al final del siguiente vídeo (en inglés):
También puedes empezar usando el enlace a mi proyecto https://stackblitz.com/edit/angular-signals-deborahk. Asegúrate de crear una copia para que puedas realizar tus propios cambios.
¡Las señales llegaron! Van a mejorar la capacidad de detección de cambios y la reactividad de tu código, además de hacerlo de más fácil edición y lectura, además que son muy divertidas de usar.