Computed Signals

Intermedio
Angular
Angular
Actualizado: 24/09/2025

Función computed() y dependencias

Los computed signals representan valores que se derivan automáticamente de otros signals. A diferencia de los signals básicos que creamos con signal(), los computed signals se recalculan de forma reactiva cada vez que alguna de sus dependencias cambia.

La función computed() crea signals de solo lectura que ejecutan una función de cálculo cuando cualquiera de los signals que leen dentro de esa función se modifica. Este mecanismo permite crear cadenas de reactividad donde los cambios se propagan automáticamente por toda la aplicación.

Creación de computed signals

Para crear un computed signal utilizamos la función computed() que recibe como parámetro una función de cálculo:

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

const precio = signal(100);
const descuento = signal(0.1);

// Computed signal que calcula el precio final
const precioFinal = computed(() => {
  return precio() * (1 - descuento());
});

console.log(precioFinal()); // 90

En este ejemplo, precioFinal es un computed signal que depende de precio y descuento. Cada vez que alguno de estos signals base cambie, precioFinal se recalculará automáticamente.

Dependencias automáticas

Una de las características más importantes de los computed signals es que Angular rastrea automáticamente qué signals se leen durante la ejecución de la función de cálculo. No necesitamos declarar explícitamente las dependencias:

const nombre = signal('Juan');
const apellido = signal('Pérez');
const edad = signal(25);

// Angular detecta automáticamente que depende de nombre y apellido
const nombreCompleto = computed(() => {
  return `${nombre()} ${apellido()}`;
});

// Este computed solo depende de edad (no de nombre ni apellido)
const esAdulto = computed(() => {
  return edad() >= 18;
});

Cuando nombre o apellido cambien, solo nombreCompleto se recalculará. Si cambia edad, solo esAdulto se actualizará. Esta detección automática de dependencias hace que el sistema sea muy eficiente.

Computed signals de solo lectura

Los computed signals son inmutables por diseño. No podemos modificar directamente su valor usando métodos como set() o update():

const contador = signal(0);
const doble = computed(() => contador() * 2);

// ❌ Esto causará un error
// doble.set(10); // Error: Property 'set' does not exist

// ✅ La única forma de cambiar un computed es modificar sus dependencias
contador.set(5);
console.log(doble()); // 10

Esta restricción garantiza que el flujo de datos sea unidireccional y predecible, evitando inconsistencias en el estado de la aplicación.

Dependencias múltiples y complejas

Los computed signals pueden depender de múltiples signals y realizar cálculos complejos:

const productos = signal([
  { nombre: 'Laptop', precio: 1000, cantidad: 2 },
  { nombre: 'Mouse', precio: 25, cantidad: 3 },
  { nombre: 'Teclado', precio: 75, cantidad: 1 }
]);

const impuesto = signal(0.21);
const descuentoGeneral = signal(0.05);

// Computed que calcula el total del carrito
const totalCarrito = computed(() => {
  const subtotal = productos().reduce((sum, producto) => {
    return sum + (producto.precio * producto.cantidad);
  }, 0);
  
  const conDescuento = subtotal * (1 - descuentoGeneral());
  return conDescuento * (1 + impuesto());
});

// Computed que cuenta los artículos
const totalArticulos = computed(() => {
  return productos().reduce((sum, producto) => sum + producto.cantidad, 0);
});

En este ejemplo, totalCarrito depende de tres signals diferentes: productos, impuesto y descuentoGeneral. Cualquier cambio en cualquiera de ellos provocará que se recalcule automáticamente.

Computed signals en componentes

Los computed signals son especialmente útiles en componentes para crear propiedades derivadas:

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

@Component({
  selector: 'app-usuario',
  standalone: true,
  template: `
    <div class="usuario-card">
      <h2>{{ nombreCompleto() }}</h2>
      <p>{{ estadoMembresía() }}</p>
      <div class="puntos">
        <span>Puntos: {{ puntosUsuario() }}</span>
        <span class="nivel">{{ nivelUsuario() }}</span>
      </div>
    </div>
  `
})
export class UsuarioComponent {
  nombre = signal('Ana');
  apellido = signal('García');
  puntos = signal(1250);
  esPremium = signal(true);

  // Computed signals que se actualizan automáticamente
  nombreCompleto = computed(() => {
    return `${this.nombre()} ${this.apellido()}`;
  });

  nivelUsuario = computed(() => {
    const puntos = this.puntosUsuario();
    if (puntos >= 2000) return 'Oro';
    if (puntos >= 1000) return 'Plata';
    return 'Bronce';
  });

  estadoMembresía = computed(() => {
    return this.esPremium() ? 'Miembro Premium' : 'Miembro Estándar';
  });

  puntosUsuario = computed(() => {
    const base = this.puntos();
    return this.esPremium() ? base * 1.5 : base;
  });
}

En este componente, todos los computed signals se actualizan automáticamente cuando cambian sus dependencias. Si esPremium cambia de false a true, tanto estadoMembresía como puntosUsuario y nivelUsuario se recalcularán automáticamente.

Cadenas de computed signals

Los computed signals pueden depender de otros computed signals, creando cadenas de reactividad:

const temperatura = signal(25); // Celsius
const unidad = signal('celsius');

// Primer nivel: conversión de temperatura
const temperaturaKelvin = computed(() => {
  return temperatura() + 273.15;
});

const temperaturaFahrenheit = computed(() => {
  return (temperatura() * 9/5) + 32;
});

// Segundo nivel: temperatura mostrada según la unidad
const temperaturaFormateada = computed(() => {
  const temp = temperatura();
  const unit = unidad();
  
  switch (unit) {
    case 'fahrenheit':
      return `${temperaturaFahrenheit().toFixed(1)}°F`;
    case 'kelvin':
      return `${temperaturaKelvin().toFixed(1)}K`;
    default:
      return `${temp.toFixed(1)}°C`;
  }
});

// Tercer nivel: mensaje basado en la temperatura
const mensajeClima = computed(() => {
  const tempC = temperatura();
  if (tempC > 30) return 'Hace calor';
  if (tempC < 10) return 'Hace frío';
  return 'Temperatura agradable';
});

En esta cadena, cuando temperatura cambia, se actualizan automáticamente temperaturaKelvin, temperaturaFahrenheit, temperaturaFormateada y mensajeClima en el orden correcto.

Lazy evaluation y caching

Los computed signals de Angular implementan dos optimizaciones fundamentales que mejoran significativamente el rendimiento: lazy evaluation (evaluación perezosa) y caching (almacenamiento en caché). Estas características garantizan que los cálculos solo se ejecuten cuando sea necesario y que los resultados se reutilicen eficientemente.

Lazy evaluation: cálculo bajo demanda

La evaluación perezosa significa que un computed signal no ejecuta su función de cálculo hasta que alguien realmente lee su valor. Esto evita cálculos innecesarios y mejora el rendimiento de la aplicación:

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

const numeros = signal([1, 2, 3, 4, 5]);

// Este computed NO se ejecuta inmediatamente
const suma = computed(() => {
  console.log('Calculando suma...');
  return numeros().reduce((acc, num) => acc + num, 0);
});

const promedio = computed(() => {
  console.log('Calculando promedio...');
  return suma() / numeros().length;
});

// Hasta aquí, NO se han ejecutado los console.log
// Los cálculos ocurren solo cuando leemos los valores

console.log(suma()); // Ahora sí se ejecuta "Calculando suma..."
console.log(promedio()); // Se ejecuta "Calculando promedio..." pero NO "Calculando suma..." otra vez

En este ejemplo, la función de suma solo se ejecuta cuando accedemos a suma(). Además, cuando leemos promedio(), este utiliza el valor ya calculado de suma sin recalcularlo.

Sistema de caching inteligente

Los computed signals almacenan en caché el resultado de sus cálculos hasta que alguna de sus dependencias cambie. Esto significa que múltiples lecturas del mismo computed signal devuelven el valor cacheado sin ejecutar la función nuevamente:

const precio = signal(100);
const descuento = signal(0.1);

const precioFinal = computed(() => {
  console.log('Ejecutando cálculo de precio final...');
  return precio() * (1 - descuento());
});

// Primera lectura: ejecuta el cálculo
console.log(precioFinal()); // "Ejecutando cálculo de precio final..." → 90

// Lecturas posteriores: devuelve el valor cacheado
console.log(precioFinal()); // NO imprime el mensaje, devuelve 90
console.log(precioFinal()); // NO imprime el mensaje, devuelve 90

// Cambio en dependencia: invalida la caché
precio.set(200);

// Primera lectura después del cambio: recalcula
console.log(precioFinal()); // "Ejecutando cálculo de precio final..." → 180
console.log(precioFinal()); // NO imprime el mensaje, devuelve 180

El sistema de caching garantiza que el cálculo solo se ejecute una vez hasta que cambie alguna dependencia, optimizando el rendimiento en aplicaciones con múltiples accesos a los mismos computed signals.

Optimización en cadenas de computed signals

En cadenas de computed signals, el caching se vuelve especialmente importante para evitar recálculos en cascada innecesarios:

const datos = signal([
  { categoria: 'A', valor: 100 },
  { categoria: 'B', valor: 200 },
  { categoria: 'A', valor: 150 }
]);

const datosPorCategoria = computed(() => {
  console.log('Agrupando datos por categoría...');
  return datos().reduce((acc, item) => {
    if (!acc[item.categoria]) acc[item.categoria] = [];
    acc[item.categoria].push(item);
    return acc;
  }, {} as Record<string, any[]>);
});

const totalPorCategoria = computed(() => {
  console.log('Calculando totales por categoría...');
  const grupos = datosPorCategoria(); // Usa el valor cacheado
  return Object.keys(grupos).reduce((acc, categoria) => {
    acc[categoria] = grupos[categoria].reduce((sum, item) => sum + item.valor, 0);
    return acc;
  }, {} as Record<string, number>);
});

const resumen = computed(() => {
  console.log('Generando resumen...');
  const totales = totalPorCategoria(); // Usa el valor cacheado
  return {
    totalGeneral: Object.values(totales).reduce((sum, total) => sum + total, 0),
    categorias: Object.keys(totales).length,
    detalle: totales
  };
});

// Primera lectura de resumen ejecuta toda la cadena una sola vez
console.log(resumen());
// "Agrupando datos por categoría..."
// "Calculando totales por categoría..."  
// "Generando resumen..."

// Lecturas posteriores usan valores cacheados
console.log(resumen().totalGeneral); // Sin mensajes de consola
console.log(totalPorCategoria()); // Sin mensajes de consola

Invalidación selectiva de caché

Cuando cambia una dependencia, solo se invalida la caché de los computed signals que realmente dependen de ella, no toda la cadena:

const configuracion = signal({ tema: 'claro', idioma: 'es' });
const usuario = signal({ nombre: 'Ana', edad: 28 });

const temaActual = computed(() => {
  console.log('Obteniendo tema...');
  return configuracion().tema;
});

const saludo = computed(() => {
  console.log('Generando saludo...');
  const { nombre } = usuario();
  return `Hola, ${nombre}`;
});

const interfaz = computed(() => {
  console.log('Configurando interfaz...');
  return {
    tema: temaActual(),
    mensaje: saludo()
  };
});

// Primera lectura ejecuta todos los cálculos
console.log(interfaz());

// Cambio solo en configuración: invalida temaActual e interfaz, pero NO saludo
configuracion.update(config => ({ ...config, tema: 'oscuro' }));

console.log(interfaz());
// Solo imprime: "Obteniendo tema..." y "Configurando interfaz..."
// NO imprime "Generando saludo..." porque su dependencia no cambió

Beneficios de rendimiento en componentes

En aplicaciones reales, lazy evaluation y caching proporcionan mejoras significativas de rendimiento, especialmente en templates con múltiples referencias al mismo computed signal:

@Component({
  selector: 'app-estadisticas',
  standalone: true,
  template: `
    <div class="estadisticas">
      <div class="card">
        <h3>Resumen</h3>
        <p>Total: {{ estadisticas().total }}</p>
        <p>Promedio: {{ estadisticas().promedio }}</p>
      </div>
      
      <div class="card">
        <h3>Distribución</h3>
        @for (categoria of estadisticas().categorias; track categoria.nombre) {
          <div>{{ categoria.nombre }}: {{ categoria.valor }}</div>
        }
      </div>
      
      <div class="card">
        <h3>Comparación</h3>
        <p>Máximo: {{ estadisticas().maximo }}</p>
        <p>Mínimo: {{ estadisticas().minimo }}</p>
      </div>
    </div>
  `
})
export class EstadisticasComponent {
  datos = signal([
    { nombre: 'Ventas', valor: 1500 },
    { nombre: 'Marketing', valor: 800 },
    { nombre: 'Desarrollo', valor: 1200 }
  ]);

  // Computed con cálculo costoso que se ejecuta solo cuando es necesario
  estadisticas = computed(() => {
    console.log('Calculando estadísticas completas...');
    const datos = this.datos();
    const valores = datos.map(d => d.valor);
    
    return {
      total: valores.reduce((sum, val) => sum + val, 0),
      promedio: valores.reduce((sum, val) => sum + val, 0) / valores.length,
      maximo: Math.max(...valores),
      minimo: Math.min(...valores),
      categorias: datos.map(d => ({
        nombre: d.nombre,
        valor: d.valor,
        porcentaje: (d.valor / valores.reduce((sum, val) => sum + val, 0)) * 100
      }))
    };
  });
}

En este componente, aunque el template accede a estadisticas() múltiples veces, el cálculo se ejecuta solo una vez durante el renderizado gracias al caching. Si los datos no cambian entre renderizados, el computed signal devuelve el valor cacheado sin recalcular.

Control de caching con dependencias condicionales

Los computed signals también optimizan el caching cuando tienen dependencias condicionales:

const modo = signal<'simple' | 'avanzado'>('simple');
const datosBasicos = signal([1, 2, 3, 4, 5]);
const datosAvanzados = signal([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);

const procesamiento = computed(() => {
  console.log('Ejecutando procesamiento...');
  const modoActual = modo();
  
  if (modoActual === 'simple') {
    // Solo depende de datosBasicos en modo simple
    return datosBasicos().map(x => x * 2);
  } else {
    // Solo depende de datosAvanzados en modo avanzado
    return datosAvanzados().map(x => x * x);
  }
});

// En modo simple, cambios en datosAvanzados NO invalidan la caché
console.log(procesamiento()); // modo: 'simple'
datosAvanzados.set([20, 30, 40]); // No recalcula porque no lee datosAvanzados
console.log(procesamiento()); // Usa valor cacheado

// Cambio a modo avanzado invalida la caché
modo.set('avanzado');
console.log(procesamiento()); // Recalcula porque cambió la dependencia

Esta optimización hace que Angular solo rastree las dependencias que realmente se leen durante cada ejecución, mejorando la eficiencia del sistema de reactividad.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Angular

Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender qué son los computed signals y cómo se crean con la función computed().
  • Entender la detección automática de dependencias y su impacto en la reactividad.
  • Aprender la inmutabilidad de los computed signals y cómo modificar sus dependencias.
  • Conocer la evaluación perezosa (lazy evaluation) y el caching para optimizar el rendimiento.
  • Aplicar computed signals en componentes y manejar cadenas complejas de dependencias reactivas.