Angular

Angular

Tutorial Angular: Signals en Angular

Angular signals: definición y uso. Aprende a usar signals en Angular con ejemplos prácticos y detallados.

¿Qué son los Signals?

Los Signals en Angular representan una abstracción para gestionar la reactividad en las aplicaciones. A diferencia de otros paradigmas reactivos, los Signals proporcionan un enfoque declarativo y estructurado, enfocado en la propagación y sincronización de datos.

Una Signal es una entidad reactiva dentro de Angular que encapsula un valor (actuando como un contenedor para dicho valor) y notifica automáticamente a los consumidores cada vez que ese valor cambia. 

Puedes pensar en las Signals de Angular como una combinación de un valor de datos y un mecanismo de notificación de cambios, ofreciendo una manera simplificada de rastrear cambios y actualizar de manera fluida la interfaz de usuario en respuesta a esos cambios.

Características principales de los Signals:

  1. Comportamiento predecible: Los Signals siguen reglas estrictas para emitir cambios, lo que ayuda a evitar efectos secundarios inesperados.
  2. Inmutabilidad controlada: Aunque permiten mutaciones, estas son controladas y explícitas, promoviendo una gestión más clara del estado.
  3. Facilidad de composición: Los Signals pueden ser combinados y derivados fácilmente, permitiendo crear relaciones complejas de manera sencilla.
  4. Optimización del rendimiento: Al ser declarativos y al permitir la reutilización lógica, los Signals pueden ayudar a minimizar los cálculos redundantes y las actualizaciones innecesarias de la UI.

Ejemplo básico de uso de un Signal

Ponemos un ejemplo donde creamos un Signal que almacena un valor numérico y reaccionamos a los cambios de ese valor.

import { signal, effect } from '@angular/core';

// Creación de un Signal writable
const count = signal(0);

// Subscribirse a los cambios del Signal
effect(() => {
  console.log('El valor actual de count es', count());
});

// Mutar el valor del Signal
count.set(1); // Consola: El valor actual de count es 1
count.update(value => value + 1); // Consola: El valor actual de count es 2

En este ejemplo, count es un Signal que inicialmente tiene un valor de 0. Cada vez que el valor de count cambia a través de set o update, la función pasada a effect se ejecuta, reflejando el nuevo valor en la consola.

Signals en Angular proporciona una base sólida para crear aplicaciones reactivas, garantizando que los cambios en los datos se gestionen eficientemente y con un comportamiento predecible.

Signals writables cómo crearlos y usarlos

Un Signal writable es un tipo de Signal en Angular que permite no solo leer su valor actual, sino también modificarlo explícitamente. Estos Signals son fundamentales para gestionar el estado mutable en aplicaciones reactivas, permitiendo que los cambios en el estado se propaguen automáticamente a los consumidores correspondientes.

Para crear un Signal writable, utilizamos la función signal del paquete @angular/core. La función signal acepta un valor inicial y devuelve un objeto Signal que proporciona métodos para leer y actualizar su valor.

Creación de un Signal writable

Aquí mostramos cómo crear un Signal writable con un valor inicial de 0:

import { signal } from '@angular/core';

// Creación de un Signal writable con valor inicial 0
const count = signal(0);

Métodos para gestionar el estado de un Signal writable

Los Signals writables proporcionan varios métodos para interactuar con el estado:

  1. set(newValue: T): Establece el valor del Signal a newValue, desencadenando cualquier efecto reactivo asociado.
  2. update(updateFn: (currentValue: T) => T): Actualiza el valor utilizando una función que recibe el valor actual y devuelve el nuevo valor.
  3. valueOf(): Retorna el valor actual del Signal.

Ejemplos prácticos

Establecer un nuevo valor

Utilizamos el método set para modificar directamente el valor del Signal:

// Establecer el valor a 10
count.set(10);
console.log(count.valueOf()); // Output: 10

Actualizar el valor basado en su estado actual

Con update, actualizamos el valor del Signal basándonos en su valor actual:

// Incrementar el valor en 1
count.update(currentValue => currentValue + 1);
console.log(count.valueOf()); // Output: 11

Integración con efectos reactivos

Para responder a los cambios en el valor de un Signal writable, utilizamos la función effect del paquete @angular/core. Esto nos permite definir comportamientos que se ejecutan automáticamente cuando cambia el valor del Signal:

import { effect } from '@angular/core';

// Crear un efecto que reacciona a los cambios en count
effect(() => {
  console.log('El valor de count ha cambiado a', count.valueOf());
});

// Modificar el valor del Signal para activar el efecto
count.set(20); // Consola: El valor de count ha cambiado a 20
count.update(value => value * 2); // Consola: El valor de count ha cambiado a 40

En este ejemplo, cada cambio en el valor de count desencadena el efecto que registra el nuevo valor en la consola.

Signals computed cómo crearlos y usarlos

Computed Signals en Angular son una abstracción poderosa que permite crear Signals derivados cuyo valor es computado automáticamente a partir de otros Signals. Estos Signals no reciben valores directamente, sino que su valor es el resultado de una función computada que depende de otros Signals.

Para crear un Signal computed, utilizamos la función computed del paquete @angular/core. Esta función acepta un callback que define cómo computar el valor del Signal a partir de otros Signals.

Creación de un Signal computed

Veamos cómo crear un Signal computed que dependa de un Signal writable existente:

import { signal, computed } from '@angular/core';

// Creación de un Signal writable
const baseValue = signal(5);

// Creación de un Signal computed que depende de baseValue
const doubleValue = computed(() => baseValue.valueOf() * 2);

console.log(doubleValue.valueOf()); // Output: 10

En este ejemplo, doubleValue es un Signal computed cuyo valor es siempre el doble del valor de baseValue. Cada vez que baseValue cambie, doubleValue se actualizará automáticamente para reflejar la nueva composición.

Reactividad de los Signals computed

Los Signals computed son reactivos por naturaleza. Esto significa que cualquier efecto o Signal computado que dependa de ellos se actualizará automáticamente cuando sus dependencias cambien:

import { effect } from '@angular/core';

// Crear un efecto que reacciona a cambios en doubleValue
effect(() => {
  console.log('El valor de doubleValue es', doubleValue.valueOf());
});

// Modificar el valor del Signal writable baseValue
baseValue.set(10); // Consola: El valor de doubleValue es 20

En este ejemplo, el efecto se ejecutará cada vez que baseValue cambie, reflexionando el nuevo valor en doubleValue.

Uso avanzado de Signals computed

Los Signals computed pueden depender de múltiples Signals y contener lógica compleja para computar sus valores:

const multiplier = signal(3);

// Computar un valor que depende de `baseValue` y `multiplier`
const computedValue = computed(() => baseValue.valueOf() * multiplier.valueOf());

console.log(computedValue.valueOf()); // Output: 30

// Reactividad a múltiples dependencias
effect(() => {
  console.log('El valor de computedValue es', computedValue.valueOf());
});

// Cambiar las dependencias
baseValue.set(4); // Consola: El valor de computedValue es 12
multiplier.set(5); // Consola: El valor de computedValue es 20

En este ejemplo, computedValue depende tanto de baseValue como de multiplier. Cada vez que cualquiera de estos Signals cambie, computedValue se volverá a computar y cualquier efecto asociado se ejecutará.

Optimización y limitaciones

A pesar de su potencia, es importante utilizar Signals computed de manera responsable para evitar complejidades innecesarias y ciclos de dependencias. La reactividad en Signals computed está optimizada para evitar recomputaciones redundantes, pero un uso excesivo o indebido puede llevar a problemas de rendimiento y complejidad en el código.

Ejemplos prácticos

Computar valores derivados

const price = signal(200);
const quantity = signal(3);

// Computar el valor total como un Signal computed
const totalPrice = computed(() => price.valueOf() * quantity.valueOf());

effect(() => {
  console.log('El precio total es', totalPrice.valueOf());
});

price.set(250); // Consola: El precio total es 750
quantity.set(4); // Consola: El precio total es 1000

Validez y verificación

Los Signals computed también se pueden usar para validar datos y derivar estados:

const username = signal('user123');
const isValidUsername = computed(() => username.valueOf().length >= 5);

effect(() => {
  console.log('¿Es el nombre de usuario válido?', isValidUsername.valueOf());
});

username.set('us'); // Consola: ¿Es el nombre de usuario válido? false
username.set('newUser'); // Consola: ¿Es el nombre de usuario válido? true

En conclusión, los Signals computed son una herramienta eficaz para derivar valores de forma reactiva y optimizada, permitiendo manejar la complejidad de aplicaciones en Angular con mayor claridad y previsibilidad.

Consumir signal en un componente

Para consumir un Signal en un componente de Angular, primero es necesario crear el Signal en un contexto adecuado y luego utilizarlo dentro del componente para que la UI reaccione a los cambios en su valor. Los Signals simplifican la reactividad al permitir que el componente se reactive automáticamente ante los cambios de estado, sin necesidad de suscribirse manualmente a observables ni manejar suscripciones complejas.

Crear un Signal en un servicio

Se recomienda encapsular los Signals en servicios para una mejor separación de responsabilidades y facilitar la prueba y reutilización del código. A continuación, creamos un servicio con un Signal writable:

import { Injectable } from '@angular/core';
import { signal } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class CounterService {
  // Creación de un Signal writable para manejar el conteo
  private _count = signal(0);

  get count() {
    return this._count;
  }

  increment() {
    this._count.update(value => value + 1);
  }

  decrement() {
    this._count.update(value => value - 1);
  }
}

Inyectar el servicio en el componente

Una vez que el servicio está configurado, inyectamos el servicio en el componente donde queremos consumir el Signal y utilizamos esa inyección para obtener y reaccionar a los cambios del Signal:

import { Component, effect } from '@angular/core';
import { CounterService } from './counter.service';

@Component({
  selector: 'app-counter',
  template: `
    <div>
      <p>Count: {{ count }}</p>
      <button (click)="increment()">Increment</button>
      <button (click)="decrement()">Decrement</button>
    </div>
  `,
  styleUrls: ['./counter.component.css']
})
export class CounterComponent {
  count: number;

  constructor(private counterService: CounterService) {
    // Consumir el Signal y actualizar el valor local en el componente
    effect(() => {
      this.count = this.counterService.count();
    });
  }

  increment() {
    this.counterService.increment();
  }

  decrement() {
    this.counterService.decrement();
  }
}

Reactividad automática en el template

El uso de Signals permite que los cambios en el valor del Signal se reflejen automáticamente en el template del componente sin necesidad de suscribirse manualmente. En el ejemplo anterior, el elemento {{ count }} en el template se actualizará automáticamente cuando cambie el Signal count.

Este enfoque no solo simplifica la lógica reactiva en el componente, sino que también mejora la claridad y la mantenibilidad del código. A medida que el estado definido por los Signals cambia, la UI del componente se re-rendea de manera eficiente y predecible.

Ejemplo completo

A continuación se muestra un ejemplo completo de un servicio y un componente que utiliza Signals para manejar el estado y la reactividad:

counter.service.ts

import { Injectable } from '@angular/core';
import { signal } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class CounterService {
  private _count = signal(0);

  get count() {
    return this._count;
  }

  increment() {
    this._count.update(value => value + 1);
  }

  decrement() {
    this._count.update(value => value - 1);
  }
}

counter.component.ts

import { Component, effect } from '@angular/core';
import { CounterService } from './counter.service';

@Component({
  selector: 'app-counter',
  template: `
    <div>
      <p>Count: {{ count }}</p>
      <button (click)="increment()">Increment</button>
      <button (click)="decrement()">Decrement</button>
    </div>
  `,
  styleUrls: ['./counter.component.css']
})
export class CounterComponent {
  count: number;

  constructor(private counterService: CounterService) {
    effect(() => {
      this.count = this.counterService.count();
    });
  }

  increment() {
    this.counterService.increment();
  }

  decrement() {
    this.counterService.decrement();
  }
}

Con esta estructura, hemos encapsulado la lógica de manejo del estado en un servicio y la hemos consumido en un componente, aprovechando la reactividad y simplicidad que proporcionan los Signals en Angular.

Compartir signals con servicios

Para compartir Signals a través de distintos componentes en Angular, es recomendable utilizar servicios. Los servicios permiten encapsular la lógica de estado independiente de la UI, facilitando la reutilización y el mantenimiento del código. Aquí explicamos cómo crear un servicio con un Signal y cómo exponerlo para que diferentes componentes puedan consumir y reaccionar a los cambios de manera eficiente.

Creación de un servicio con Signals

Empezamos por crear un servicio en Angular que contendrá nuestro Signal. Este servicio encapsulará tanto el Signal como los métodos necesarios para interactuar con él.

import { Injectable } from '@angular/core';
import { signal } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class SharedService {
  private _sharedSignal = signal<boolean>(false);

  get sharedSignal() {
    return this._sharedSignal;
  }

  toggleSignal() {
    this._sharedSignal.update(value => !value);
  }
}

En este ejemplo, SharedService contiene un Signal booleano sharedSignal inicializado a false, y un método toggleSignal que invierte su valor.

Inyectar el servicio en componentes

Para que múltiples componentes puedan compartir el estado encapsulado en el servicio, debemos inyectar el servicio en cada componente que desee consumir el Signal.

Componente A

import { Component, effect } from '@angular/core';
import { SharedService } from './shared.service';

@Component({
  selector: 'app-component-a',
  template: `
    <div>
      <p>Componente A - Estado: {{ state }}</p>
      <button (click)="toggleState()">Toggle State</button>
    </div>
  `,
  styleUrls: ['./component-a.component.css']
})
export class ComponentA {
  state: boolean;

  constructor(private sharedService: SharedService) {
    effect(() => {
      this.state = this.sharedService.sharedSignal();
    });
  }

  toggleState() {
    this.sharedService.toggleSignal();
  }
}

Componente B

import { Component, effect } from '@angular/core';
import { SharedService } from './shared.service';

@Component({
  selector: 'app-component-b',
  template: `
    <div>
      <p>Componente B - Estado: {{ state }}</p>
    </div>
  `,
  styleUrls: ['./component-b.component.css']
})
export class ComponentB {
  state: boolean;

  constructor(private sharedService: SharedService) {
    effect(() => {
      this.state = this.sharedService.sharedSignal();
    });
  }
}

Reactividad compartida

En el ejemplo anterior, tanto ComponentA como ComponentB están inyectando SharedService y suscribiéndose a sharedSignal. Cuando toggleState se llama en ComponentA, el valor de sharedSignal cambia y ambos componentes reaccionan al cambio automáticamente debido a su dependencia reactiva.

Beneficios de compartir Signals con servicios

  1. Separation of Concerns (SoC): La lógica de estado y reactividad se encapsula en un servicio, separando la manipulación del estado de la representación de la UI.
  2. Reusabilidad: Un servicio puede ser reutilizado por múltiples componentes, promoviendo la cohesividad y reduciendo duplicaciones de código.
  3. Mantenibilidad: Actualizar o cambiar la lógica en el servicio afecta automáticamente a todos los componentes que lo consumen, eliminando la necesidad de modificar cada componente individualmente.
  4. Simplicidad: Simplifica la lógica de los componentes, evitando suscripciones explícitas a observables y gestión manual de suscripciones.

Utilizar servicios para compartir Signals es una práctica recomendada en aplicaciones Angular, permitiendo aprovechar al máximo las capacidades reactivas y declarativas que los Signals proporcionan.

Signals RxJS Interop toSignal() y toObservable()

Angular proporciona integración fluida entre Signals y RxJS a través de métodos dedicados que permiten convertir un Signal en un Observable y viceversa. Esta capacidad interopera entre el modelo de reactividad basado en Signals y el ecosistema RxJS, permitiendo a los desarrolladores combinar lo mejor de ambos mundos según lo requiera la aplicación.

Conversión de un Observable a un Signal (toSignal())

El método toSignal() permite convertir un Observable de RxJS en un Signal en Angular. Esto es útil para integrar flujos de datos existentes de observables, tales como datos de una API, servicios de tiempo real, etc., en el ecosistema de Signals.

Para convertir un Observable a un Signal:

import { toSignal } from '@angular/core/rxjs-interop';
import { of } from 'rxjs';

const myObservable = of(1, 2, 3);
const mySignal = toSignal(myObservable);

// mySignal ahora es un Signal que emitirá los valores 1, 2, y 3 secuencialmente

Los valores que emite el Observable se convertirán en el estado reactivo del Signal, proporcionando compatibilidad directa con el ecosistema de Signals.

Conversión de un Signal a un Observable (toObservable())

El método toObservable() permite convertir un Signal en un Observable de RxJS, facilitando la interoperabilidad cuando se desee usar infraestructura de RxJS, como operadores o combinadores.

Para convertir un Signal a un Observable:

import { signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { map } from 'rxjs/operators';

// Crear un Signal
const mySignal = signal(10);

// Convertir el Signal a un Observable
const myObservable = toObservable(mySignal);

// Usar operadores de RxJS en el Observable
const mappedObservable = myObservable.pipe(map(value => value * 2));

mappedObservable.subscribe(value => console.log(value)); // Salida: 20

En este ejemplo, mySignal se convierte en un Observable, al cual se le aplica el operador map de RxJS, multiplicando así el valor emitido por 2 antes de suscribirse.

Ejemplo práctico de Signals y RxJS interop

Imaginemos un escenario donde tenemos un servicio que obtiene datos de un servidor a través de un Observable y queremos consumir esos datos en un componente utilizando Signals.

Servicio Angular con Observable

import { Injectable } from '@angular/core';
import { Observable, interval } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  getData(): Observable<number> {
    return interval(1000).pipe(map(val => val * 2)); // Emite un valor cada segundo, multiplicado por 2
  }
}

Componente Angular utilizando Signal

import { Component, effect } from '@angular/core';
import { DataService } from './data.service';
import { toSignal } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-data-display',
  template: `
    <div>Data: {{ data }}</div>
  `
})
export class DataDisplayComponent {
  data: number;

  constructor(private dataService: DataService) {
    // Convertir el Observable a un Signal
    const dataSignal = toSignal(this.dataService.getData());
    effect(() => {
      this.data = dataSignal();
    });
  }
}

En este ejemplo completo, el DataService proporciona datos reactivos a través de un Observable que emite nuevos valores cada segundo. El DataDisplayComponent convierte este Observable a un Signal y lo consume directamente, permitiendo que la UI se actualice automáticamente con cada nuevo valor emitido.

Utilizar toSignal() y toObservable() facilita una integración eficiente y elegante entre Signals y RxJS dentro de aplicaciones Angular, aprovechando la reactividad declarativa de Signals sin renunciar a las potentes capacidades de procesamiento de datos de RxJS.

Certifícate en Angular con CertiDevs PLUS

Ejercicios de esta lección Signals en Angular

Evalúa tus conocimientos de esta lección Signals en Angular con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Signals en Angular

Angular
Puzzle

Guards funcionales

Angular
Test

Decodificar JWT en Angular

Angular
Test

Servicio con HttpClient

Angular
Código

Ciclo de vida de componentes en Angular

Angular
Test

Gestión de productos de Fake Store API

Angular
Proyecto

Data binding en Angular

Angular
Test

Routes sin módulos en Angular

Angular
Código

Router en Angular

Angular
Test

Instalación de Angular

Angular
Puzzle

Route Guards basados en interfaces

Angular
Código

La directiva @if en Angular

Angular
Puzzle

Formularios reactivos en Angular

Angular
Código

Servicios en Angular

Angular
Puzzle

Interceptor funcional

Angular
Test

Servicio con Array

Angular
Código

La directiva @for en Angular

Angular
Puzzle

Interceptores HTTP

Angular
Código

Componentes standalone true

Angular
Puzzle

Formularios con ngModel en Angular

Angular
Puzzle

Routes en Angular

Angular
Test

Comunicación entre componentes Angular

Angular
Test

Parámetros en rutas con ActivatedRoute

Angular
Test

CRUD de Restaurantes y Platos

Angular
Proyecto

Tablas en Angular Material

Angular
Puzzle

Formulario de registro de usuarios

Angular
Proyecto

Instalación y uso de NgBoostrap

Angular
Puzzle

Desarrollo de componentes Angular

Angular
Test

JWT en Angular

Angular
Código

Formularios reactivos en Angular

Angular
Puzzle

Formularios en Angular Material

Angular
Puzzle

Layout con Angular Material

Angular
Puzzle

Effects en Angular

Angular
Test

Data binding

Angular
Código

HttpClient en servicios de Angular

Angular
Puzzle

Desarrollo de módulos Angular

Angular
Puzzle

Comandos Angular CLI

Angular
Puzzle

Subir archivo en formularios

Angular
Test

La directiva routerLink en Angular

Angular
Test

Todas las lecciones de Angular

Accede a todas las lecciones de Angular y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Instalación Angular

Angular

Introducción Y Entorno

Comandos Angular Cli

Angular

Introducción Y Entorno

Desarrollo De Componentes Angular

Angular

Componentes

Data Binding En Angular

Angular

Componentes

Ciclo De Vida De Componentes En Angular

Angular

Componentes

Comunicación Entre Componentes Angular

Angular

Componentes

La Directiva @If En Angular

Angular

Componentes

La Directiva @For En Angular

Angular

Componentes

Componentes Standalone

Angular

Componentes

Desarrollo De Módulos Angular

Angular

Módulos

Routes En Angular

Angular

Enrutado Y Navegación

Router En Angular

Angular

Enrutado Y Navegación

La Directiva Routerlink En Angular

Angular

Enrutado Y Navegación

Parámetros En Rutas Con Activatedroute

Angular

Enrutado Y Navegación

Routes Sin Módulos En Angular

Angular

Enrutado Y Navegación

Servicios En Angular

Angular

Servicios E Inyección De Dependencias

Httpclient En Servicios De Angular

Angular

Servicios E Inyección De Dependencias

Formularios Con Ngmodel En Angular

Angular

Formularios

Formularios Reactivos En Angular

Angular

Formularios

Subir Archivo En Formularios

Angular

Formularios

Layout Con Angular Material

Angular

Integración Con Angular Material

Tablas En Angular Material

Angular

Integración Con Angular Material

Formularios En Angular Material

Angular

Integración Con Angular Material

Instalación Y Uso De Ngboostrap

Angular

Integración Con Bootstrap Css

Signals En Angular

Angular

Signals Y Reactividad

Effects En Angular

Angular

Signals Y Reactividad

Route Guards Basados En Interfaces

Angular

Autenticación Y Autorización

Guards Funcionales

Angular

Autenticación Y Autorización

Interceptores Http Basados En Interfaz

Angular

Autenticación Y Autorización

Interceptores Http Funcionales

Angular

Autenticación Y Autorización

Seguridad Jwt En Angular

Angular

Autenticación Y Autorización

Decodificar Tokens Jwt En Angular

Angular

Autenticación Y Autorización

Certificados de superación de Angular

Supera todos los ejercicios de programación del curso de Angular y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.

En esta lección

Objetivos de aprendizaje de esta lección

  • Definir Signals en Angular.
  • Consumir Signals en componentes.
  • Compartir y gestionar Signals a través de servicios.
  • Manejar estados más complejos con Signals.