Angular Signals es la característica más importante que ha llegado a Angular en los últimos años. Introducida como estable en Angular 17 y refinada en Angular 21, la API de Signals cambia fundamentalmente cómo Angular gestiona la reactividad y la detección de cambios. Si ya conoces Angular y quieres mantenerte al día, entender Signals es absolutamente prioritario en 2026.

¿Qué son los Signals en Angular?

Un Signal es un contenedor reactivo que almacena un valor y notifica automáticamente a los consumidores cuando ese valor cambia. El concepto no es nuevo (React, Vue y SolidJS llevan años con algo similar), pero la implementación de Angular es particularmente elegante e integrada con el framework.

Antes de Signals, Angular usaba Zone.js para detectar cambios: un monkey-patching de todas las APIs asíncronas del navegador para saber cuándo podía haber cambiado algo en el estado de la aplicación. Esto funcionaba, pero tenía un problema: era ineficiente. Angular no sabía exactamente qué había cambiado, solo sabía que algo podía haber cambiado, por lo que re-evaluaba el árbol de componentes completo.

Con Signals, Angular sabe exactamente qué Signal ha cambiado y qué partes del template dependen de ese Signal. El resultado es una detección de cambios mucho más precisa y eficiente, y eventualmente la posibilidad de eliminar Zone.js por completo.

Las tres primitivas principales: signal, computed y effect

signal(): estado reactivo

La función signal() crea un Signal con un valor inicial. Para leer el valor, llamas al Signal como función. Para actualizarlo, usas .set(), .update() o .mutate():

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

@Component({
  selector: 'app-contador',
  standalone: true,
  template: `
    <div class="contador">
      <h2>Contador: {{ cuenta() }}</h2>
      <button (click)="incrementar()">+1</button>
      <button (click)="decrementar()">-1</button>
      <button (click)="resetear()">Reset</button>
    </div>
  `
})
export class ContadorComponent {
  // Crear un Signal con valor inicial 0
  cuenta = signal(0);

  incrementar() {
    // .update() recibe el valor anterior y devuelve el nuevo
    this.cuenta.update(valor => valor + 1);
  }

  decrementar() {
    this.cuenta.update(valor => valor - 1);
  }

  resetear() {
    // .set() establece un valor directamente
    this.cuenta.set(0);
  }
}

computed(): valores derivados

computed() crea un Signal de solo lectura cuyo valor se calcula a partir de otros Signals. Se recalcula automáticamente cuando alguno de sus Signals dependientes cambia, pero solo se evalúa si alguien lo consume (lazy evaluation):

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

@Component({
  selector: 'app-carrito',
  standalone: true,
  template: `
    <div>
      <p>Artículos: {{ articulos() }}</p>
      <p>Precio base: {{ precioBase() | currency:'EUR' }}</p>
      <p>IVA (21%): {{ iva() | currency:'EUR' }}</p>
      <p><strong>Total: {{ total() | currency:'EUR' }}</strong></p>
      <p [class.gratis]="envioGratis()">
        Envío: {{ envioGratis() ? 'GRATIS' : '4,99 €' }}
      </p>
    </div>
  `
})
export class CarritoComponent {
  articulos = signal(3);
  precioBase = signal(45.50);

  // Signals computados: se recalculan solos cuando precioBase cambia
  iva = computed(() => this.precioBase() * 0.21);
  total = computed(() => this.precioBase() + this.iva());
  envioGratis = computed(() => this.precioBase() >= 50);
}

effect(): efectos secundarios reactivos

effect() ejecuta una función automáticamente cuando alguno de los Signals que lee dentro de ella cambia. Es el equivalente a useEffect de React o watch de Vue, pero más elegante:

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

@Component({
  selector: 'app-usuario',
  standalone: true,
  template: `
    <input [value]="nombre()" (input)="nombre.set($any($event.target).value)">
    <p>Hola, {{ nombre() }}!</p>
  `
})
export class UsuarioComponent implements OnInit {
  nombre = signal('TuRinconDev');
  
  constructor() {
    // effect() se ejecuta cada vez que nombre() cambia
    effect(() => {
      console.log('Nombre cambiado a:', this.nombre());
      // Guardar en localStorage automáticamente
      localStorage.setItem('nombre-usuario', this.nombre());
    });
  }
}

Signals con objetos y arrays

Cuando trabajas con arrays u objetos, hay un detalle importante: Angular detecta los cambios por referencia, no por valor. Esto significa que si mutas el array directamente (sin cambiar la referencia), el Signal no detectará el cambio. La solución es siempre crear una nueva referencia:

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

interface Tarea {
  id: number;
  texto: string;
  completada: boolean;
}

@Component({
  selector: 'app-tareas',
  standalone: true,
  template: `
    <div>
      <h2>Tareas ({{ pendientes() }} pendientes)</h2>
      
      <input #nuevaTarea placeholder="Nueva tarea...">
      <button (click)="agregar(nuevaTarea.value); nuevaTarea.value=''">Agregar</button>

      @for (tarea of tareas(); track tarea.id) {
        <div [class.completada]="tarea.completada">
          <input type="checkbox"
                 [checked]="tarea.completada"
                 (change)="toggleTarea(tarea.id)">
          {{ tarea.texto }}
          <button (click)="eliminar(tarea.id)">✕</button>
        </div>
      }
    </div>
  `
})
export class TareasComponent {
  tareas = signal<Tarea[]>([
    { id: 1, texto: 'Aprender Angular Signals', completada: false },
    { id: 2, texto: 'Hacer un proyecto con Signals', completada: false }
  ]);

  pendientes = computed(() =>
    this.tareas().filter(t => !t.completada).length
  );

  private nextId = 3;

  agregar(texto: string) {
    if (!texto.trim()) return;
    // Siempre spread para crear nueva referencia
    this.tareas.update(lista => [
      ...lista,
      { id: this.nextId++, texto: texto.trim(), completada: false }
    ]);
  }

  toggleTarea(id: number) {
    this.tareas.update(lista =>
      lista.map(t => t.id === id ? { ...t, completada: !t.completada } : t)
    );
  }

  eliminar(id: number) {
    this.tareas.update(lista => lista.filter(t => t.id !== id));
  }
}

Signals con servicios: estado compartido entre componentes

Uno de los casos de uso más potentes de Signals es compartir estado entre múltiples componentes a través de un servicio. Esto reemplaza muchos casos donde antes necesitabas NgRx o BehaviorSubject de RxJS:

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

export interface Usuario {
  id: number;
  nombre: string;
  email: string;
  rol: 'admin' | 'editor' | 'lector';
}

@Injectable({ providedIn: 'root' })
export class AuthService {
  // Signal privado: solo el servicio puede modificarlo
  private _usuario = signal<Usuario | null>(null);

  // Signals públicos de solo lectura: cualquiera puede leerlos
  readonly usuario = this._usuario.asReadonly();
  readonly estaAutenticado = computed(() => this._usuario() !== null);
  readonly esAdmin = computed(() => this._usuario()?.rol === 'admin');
  readonly nombreUsuario = computed(() => this._usuario()?.nombre ?? 'Invitado');

  login(email: string, password: string): Promise<boolean> {
    // Simulación de login
    return new Promise(resolve => {
      setTimeout(() => {
        if (email === 'admin@turincondev.com') {
          this._usuario.set({
            id: 1,
            nombre: 'Adam',
            email,
            rol: 'admin'
          });
          resolve(true);
        } else {
          resolve(false);
        }
      }, 500);
    });
  }

  logout() {
    this._usuario.set(null);
  }
}

// Uso en un componente
import { Component, inject } from '@angular/core';
import { AuthService } from './auth.service';

@Component({
  selector: 'app-navbar',
  standalone: true,
  template: `
    @if (auth.estaAutenticado()) {
      <span>Hola, {{ auth.nombreUsuario() }}</span>
      @if (auth.esAdmin()) {
        <a routerLink="/admin">Panel de admin</a>
      }
      <button (click)="auth.logout()">Salir</button>
    } @else {
      <a routerLink="/login">Iniciar sesión</a>
    }
  `
})
export class NavbarComponent {
  auth = inject(AuthService);
}

Signals vs RxJS: ¿cuándo usar cada uno?

Una pregunta frecuente es si Signals reemplaza a RxJS. La respuesta corta es: no completamente. Signals y RxJS tienen casos de uso diferentes y complementarios:

Caso de usoSignalsRxJS Observable
Estado de la UI (contadores, formularios)✅ Ideal⚠️ Demasiado complejo
Estado compartido entre componentes✅ Ideal✅ BehaviorSubject
Peticiones HTTP⚠️ Con toSignal()✅ Ideal (HttpClient)
Streams de eventos en tiempo real❌ No adecuado✅ Ideal (WebSocket)
Operaciones asíncronas complejas❌ No adecuado✅ Ideal (mergeMap, etc.)
Curva de aprendizaje✅ Muy baja⚠️ Media-alta

Angular proporciona las funciones toSignal() y toObservable() para convertir entre ambos mundos, lo que permite usar lo mejor de cada uno según el caso:

import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { inject, Component } from '@angular/core';

@Component({
  selector: 'app-productos',
  standalone: true,
  template: `
    @if (productos()) {
      @for (p of productos(); track p.id) {
        <div>{{ p.nombre }} - {{ p.precio | currency:'EUR' }}</div>
      }
    } @else {
      <p>Cargando...</p>
    }
  `
})
export class ProductosComponent {
  private http = inject(HttpClient);

  // Convierte un Observable de HttpClient a un Signal
  productos = toSignal(
    this.http.get<any[]>('/api/productos'),
    { initialValue: null }
  );
}

Signals e inputs: la nueva API de inputs reactivos

Angular 21 también ha estabilizado la nueva API de input() basada en Signals. Ahora los inputs de los componentes también son Signals, lo que permite crear computados directamente a partir de ellos:

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

@Component({
  selector: 'app-tarjeta-producto',
  standalone: true,
  template: `
    <div class="tarjeta">
      <h3>{{ nombre() }}</h3>
      <p class="precio">{{ precioFormateado() }}</p>
      @if (descuento() > 0) {
        <span class="badge">-{{ descuento() }}%</span>
      }
    </div>
  `
})
export class TarjetaProductoComponent {
  // input() es ahora un Signal
  nombre = input.required<string>();
  precio = input.required<number>();
  descuento = input<number>(0); // con valor por defecto

  // Computed basado en inputs (imposible con la API anterior)
  precioFormateado = computed(() => {
    const precioFinal = this.precio() * (1 - this.descuento() / 100);
    return precioFinal.toLocaleString('es-ES', { style: 'currency', currency: 'EUR' });
  });
}

Conclusión

Angular Signals representa un cambio de paradigma en cómo Angular gestiona el estado y la reactividad. La API es intuitiva, el código resultante es más legible, el rendimiento mejora significativamente y la curva de aprendizaje es mucho menor que RxJS para los casos de uso más comunes.

Si ya conoces Angular, empezar a usar Signals hoy en tus nuevos componentes es la mejor inversión que puedes hacer. Y si estás empezando con Angular, Signals es la forma moderna de gestionar el estado desde el primer día. El futuro de Angular es reactivo, y ese futuro ya está aquí. ¿Ya estás usando Signals en tus proyectos? Comparte tu experiencia en los comentarios.