<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Angular Signals vs RXJS: Cuándo usar uno y cuándo el otro

Este artículo explica la diferencia entre Angular Signals y RxJS, cuándo usar uno, cuándo usar el otro y cómo combinarlos en aplicaciones Angular reales.


Angular Signals y RxJS no son dos herramientas en guerra entre sí. Son dos herramientas diferentes, creadas para resolver problemas diferentes y, en las aplicaciones Angular modernas, a menudo funcionan muy bien juntas.

El problema surge cuando los comparamos como si fueran cien por cien intercambiables. En realidad, las señales son perfectas para representar un estado actual, legible y derivable sincrónicamente. RxJS, por otro lado, es perfecto para modelar flujos asincrónicos en el tiempo, eventos, eliminaciones, rebotes, reintentos, WebSockets y combinaciones complejas de flujos.

La pregunta correcta, por tanto, no es: “¿Señales o RxJS?”. La pregunta correcta es: “¿Estoy gestionando un estado actual o estoy gestionando un flujo de eventos a lo largo del tiempo?”.


El modelo mental: valor presente versus flujo en el tiempo

A Signal es como una variable reactiva. Contiene un valor actual. Cuando ese valor cambia, Angular sabe qué partes de la interfaz deben actualizarse.

Un Observable RxJS es una secuencia de valores a lo largo del tiempo. Puede generar cero, uno o muchos valores. Puede completarse, puede arrojar un error, se puede cancelar la suscripción y se puede transformar con operadores como map, filter, switchMap, debounceTime, reintentar y muchos más.

Escenario Señales RxJS
Estado de UI local Excelente Posible, pero a menudo excesivo
Valores derivados Excelente con calculado Posible con operadores
Llamada única HTTP Útil para guardar el resultado Genial porque HttpClient devuelve Observable
Búsqueda antirrebote Para combinar con RxJS Excelente
WebSocket Útil para mostrar el último estado Excelente para streaming
Eliminación de solicitudes No es su mejor caso Excelente con switchMap
Plantilla Angular Muy legible Muy bien con async pipe

¿Qué son las señales angulares?

Las señales son un sistema de capacidad de respuesta integrado en Angular. Una señal contiene un valor y permite que Angular rastree automáticamente quién lee ese valor.

Cuando un componente lee una señal en la plantilla, Angular puede actualizar esa parte de la interfaz de usuario cuando la señal cambia. Esto hace que el estado sea más explícito y reduce la necesidad de muchos patrones basados en Sujeto o ComportamientoSujeto para casos simples.

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

const count = signal(0);

const double = computed(() => count() * 2);

function increment(): void {
  count.update((value) => value + 1);
}

En este ejemplo, count es el estado principal, mientras que double es un estado derivado. No tenemos que actualizar manualmente double: Angular lo recalcula cuando count.

cambia

Cuando las señales son ideales

  • Cuando necesita administrar el estado local de un componente.
  • Cuando necesitas mostrar u ocultar partes de la interfaz de usuario.
  • Cuando necesitas administrar filtros locales.
  • Cuando necesita calcular valores derivados.
  • Cuando quieras evitar suscripciones manuales innecesarias.
  • Cuando deseas crear una tienda sencilla para una función.
  • Cuando el estado es sincrónico y siempre representa “el valor actual”.

¿Qué es RxJS en Angular?

RxJS es una biblioteca de programación reactiva basada en Observable. En Angular se ha utilizado durante muchos años y sigue siendo fundamental para muchas de las API del marco.

HttpClient devuelve Observable. Las formas reactivas exponen valueChanges y statusChanges. El enrutador expone parámetros, parámetros de consulta y eventos como Observables. WebSockets, temporizadores, eventos DOM y flujos complejos también modelan muy bien con RxJS.

this.searchControl.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap((query) => this.api.searchProducts(query))
);

Este ejemplo es típico: el usuario escribe en un campo de búsqueda, esperamos 300 milisegundos, ignoramos valores iguales y eliminamos automáticamente la solicitud anterior si llega una nueva búsqueda.

Cuando RxJS es ideal

  • Cuando tienes eventos asincrónicos en el tiempo.
  • Cuando necesitas rebotar o acelerar.
  • Cuando necesita eliminar solicitudes anteriores.
  • Cuando necesita combinar varias fuentes asincrónicas.
  • Cuando se trabaja con WebSockets o transmisiones en vivo.
  • Cuando necesita administrar el reintento, el tiempo de espera o el sondeo.
  • Cuando necesitas orquestar flujos complejos.

Regla general sencilla

Utilice Signals para representar el estado actual de la interfaz de usuario.

Utilice calculado para valores derivados del estado.

Utilice RxJS para eventos asincrónicos, flujos de tiempo y lógica de borrado.

Utilice toSignal cuando desee exponer un Observable como estado legible en la plantilla.

Utilice toObservable cuando una señal necesite ingresar a una canalización RxJS.


Ejemplo real 1: lista de productos con filtros locales

Imaginemos una página de comercio electrónico con una lista de productos ya cargados. El usuario puede filtrar por texto, categoría y disponibilidad. En este caso, las señales son una elección muy natural.

El estado principal consta de productos, consulta, categoría seleccionada y bandera "solo disponible". Los productos filtrados son un estado derivado.

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

export interface Product {
  id: number;
  name: string;
  category: string;
  price: number;
  stock: number;
}

@Injectable()
export class ProductsStore {
  private readonly api = inject(ProductsApiService);

  private readonly _products = signal<Product[]>([]);
  private readonly _loading = signal(false);
  private readonly _error = signal<string | null>(null);

  private readonly _query = signal('');
  private readonly _category = signal<string | null>(null);
  private readonly _onlyAvailable = signal(false);

  readonly products = this._products.asReadonly();
  readonly loading = this._loading.asReadonly();
  readonly error = this._error.asReadonly();

  readonly query = this._query.asReadonly();
  readonly category = this._category.asReadonly();
  readonly onlyAvailable = this._onlyAvailable.asReadonly();

  readonly filteredProducts = computed(() => {
    const query = this._query().toLowerCase().trim();
    const category = this._category();
    const onlyAvailable = this._onlyAvailable();

    return this._products().filter((product) => {
      const matchesQuery = product.name.toLowerCase().includes(query);
      const matchesCategory = !category || product.category === category;
      const matchesAvailability = !onlyAvailable || product.stock > 0;

      return matchesQuery && matchesCategory && matchesAvailability;
    });
  });

  readonly total = computed(() => this.filteredProducts().length);

  async loadProducts(): Promise<void> {
    this._loading.set(true);
    this._error.set(null);

    try {
      const products = await firstValueFrom(this.api.getProducts());
      this._products.set(products);
    } catch {
      this._error.set('Impossibile caricare i prodotti.');
    } finally {
      this._loading.set(false);
    }
  }

  setQuery(query: string): void {
    this._query.set(query);
  }

  setCategory(category: string | null): void {
    this._category.set(category);
  }

  setOnlyAvailable(value: boolean): void {
    this._onlyAvailable.set(value);
  }
}

Esta tienda es fácil de leer. El dato principal es la señal. Se calculan los datos derivados. El componente puede simplemente mostrar el estado.

import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';

@Component({
  selector: 'app-products-page',
  standalone: true,
  template: `
    <section class="products-page">
      <h2>Prodotti</h2>

      <input
        type="search"
        placeholder="Cerca prodotto..."
        [value]="store.query()"
        (input)="store.setQuery($any($event.target).value)"
      />

      <label>
        <input
          type="checkbox"
          [checked]="store.onlyAvailable()"
          (change)="store.setOnlyAvailable($any($event.target).checked)"
        />
        Solo disponibili
      </label>

      @if (store.loading()) {
        <p>Caricamento...</p>
      } @else if (store.error()) {
        <p class="error">{{ store.error() }}</p>
      } @else {
        <p>Risultati: {{ store.total() }}</p>

        @for (product of store.filteredProducts(); track product.id) {
          <article class="product-card">
            <h3>{{ product.name }}</h3>
            <p>Categoria: {{ product.category }}</p>
            <p>Prezzo: {{ product.price }} €</p>
          </article>
        }
      }
    </section>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductsPageComponent implements OnInit {
  readonly store = inject(ProductsStore);

  ngOnInit(): void {
    void this.store.loadProducts();
  }
}

Aquí sería posible usar RxJS para cada filtro pequeño, pero probablemente sea más detallado. Las señales hacen que el código sea más directo.


Ejemplo real 2: búsqueda backend con antirrebote y limpieza

Ahora imaginemos una búsqueda que consulta el backend cada vez que cambia el texto. En este caso, RxJS resulta mucho más adecuado.

La razón es simple: no queremos enviar una solicitud para cada personaje. Queremos esperar un pequeño intervalo, ignorar valores iguales, cancelar la solicitud anterior y manejar cualquier error.

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import {
  catchError,
  debounceTime,
  distinctUntilChanged,
  map,
  of,
  shareReplay,
  startWith,
  switchMap
} from 'rxjs';

@Component({
  selector: 'app-product-search',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <section>
      <h2>Cerca prodotti</h2>

      <input
        type="search"
        [formControl]="searchControl"
        placeholder="Scrivi almeno 2 caratteri..."
      />

      @if (results$ | async; as results) {
        @for (product of results; track product.id) {
          <p>{{ product.name }}</p>
        }
      }
    </section>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductSearchComponent {
  private readonly api = inject(ProductsApiService);

  readonly searchControl = new FormControl('', { nonNullable: true });

  readonly results$ = this.searchControl.valueChanges.pipe(
    startWith(this.searchControl.value),
    map((value) => value.trim()),
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((query) => {
      if (query.length < 2) {
        return of([]);
      }

      return this.api.searchProducts(query).pipe(
        catchError(() => of([]))
      );
    }),
    shareReplay({ bufferSize: 1, refCount: true })
  );
}

Aquí switchMap es crucial. Si el usuario escribe "iph" y luego inmediatamente "iphone", la solicitud anterior se puede cancelar y el componente solo utilizará el resultado más reciente.

Este es un ejemplo clásico en el que RxJS es más natural que Signals.


Ejemplo real 3: uso conjunto de RxJS y señales

En muchos casos, la mejor solución es usar RxJS para el flujo asincrónico y Signals para exponer el resultado a la interfaz de usuario.

Podemos comenzar desde un Observable y convertirlo en señal con toSignal.

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import {
  catchError,
  debounceTime,
  distinctUntilChanged,
  map,
  of,
  startWith,
  switchMap
} from 'rxjs';

@Component({
  selector: 'app-product-search-signal',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <section>
      <input
        type="search"
        [formControl]="searchControl"
        placeholder="Cerca prodotto..."
      />

      @for (product of results(); track product.id) {
        <p>{{ product.name }}</p>
      }
    </section>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductSearchSignalComponent {
  private readonly api = inject(ProductsApiService);

  readonly searchControl = new FormControl('', { nonNullable: true });

  private readonly results$ = this.searchControl.valueChanges.pipe(
    startWith(this.searchControl.value),
    map((value) => value.trim()),
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((query) => {
      if (query.length < 2) {
        return of([]);
      }

      return this.api.searchProducts(query).pipe(
        catchError(() => of([]))
      );
    })
  );

  readonly results = toSignal(this.results$, {
    initialValue: [] as Product[]
  });
}

El pipeline sigue siendo RxJS, porque tiene que manejar el tiempo, el rebote y la eliminación. Sin embargo, la plantilla lee una señal, por lo que es más sencilla.


Ejemplo real 4: detalle de producto con parámetro de ruta

Otro escenario muy común es una página de detalles. El producto a cargar depende del id presente en la ruta.

El parámetro de ruta es una secuencia. La llamada HTTP es una secuencia. La interfaz de usuario, por otro lado, sólo quiere el estado actual del producto.

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { catchError, distinctUntilChanged, map, of, switchMap } from 'rxjs';

@Component({
  selector: 'app-product-detail-page',
  standalone: true,
  template: `
    @if (product(); as product) {
      <article>
        <h2>{{ product.name }}</h2>
        <p>{{ product.description }}</p>
        <p>Prezzo: {{ product.price }} €</p>
      </article>
    } @else {
      <p>Prodotto non trovato.</p>
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductDetailPageComponent {
  private readonly route = inject(ActivatedRoute);
  private readonly api = inject(ProductsApiService);

  private readonly product$ = this.route.paramMap.pipe(
    map((params) => params.get('id')),
    distinctUntilChanged(),
    switchMap((id) => {
      if (!id) {
        return of(null);
      }

      return this.api.getProductById(id).pipe(
        catchError(() => of(null))
      );
    })
  );

  readonly product = toSignal(this.product$, {
    initialValue: null as Product | null
  });
}

Aquí la mejor opción no es solo Signals o solo RxJS. La mejor opción es usar ambos: RxJS para reaccionar al cambio de rumbo y Signals para mostrar el valor actual en la plantilla.


Ejemplo real 5: carrito y total del pedido

Un carrito es un ejemplo perfecto de Señales. El carrito contiene una lista de filas. De esas líneas podemos derivar la cantidad total, el subtotal, los impuestos y el total final.

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

export interface CartItem {
  productId: number;
  name: string;
  quantity: number;
  unitPrice: number;
}

@Injectable({ providedIn: 'root' })
export class CartStore {
  private readonly _items = signal<CartItem[]>([]);

  readonly items = this._items.asReadonly();

  readonly totalQuantity = computed(() =>
    this._items().reduce((total, item) => total + item.quantity, 0)
  );

  readonly subtotal = computed(() =>
    this._items().reduce(
      (total, item) => total + item.quantity * item.unitPrice,
      0
    )
  );

  readonly vat = computed(() => this.subtotal() * 0.22);

  readonly total = computed(() => this.subtotal() + this.vat());

  addItem(item: CartItem): void {
    this._items.update((items) => {
      const existing = items.find((current) => current.productId === item.productId);

      if (!existing) {
        return [...items, item];
      }

      return items.map((current) =>
        current.productId === item.productId
          ? { ...current, quantity: current.quantity + item.quantity }
          : current
      );
    });
  }

  removeItem(productId: number): void {
    this._items.update((items) =>
      items.filter((item) => item.productId !== productId)
    );
  }

  clear(): void {
    this._items.set([]);
  }
}

En este caso sería posible usar RxJS, pero las señales son más simples. El total siempre se deriva del estado actual. No es necesario pensar en una cronología de los acontecimientos.


Ejemplo real 6: Notificaciones en vivo con WebSocket

Las notificaciones en vivo, por otro lado, son un caso mucho más adecuado para RxJS. Un WebSocket emite mensajes a lo largo del tiempo. Puede caerse, reconectarse, emitir errores, producir eventos continuos.

import { Injectable } from '@angular/core';
import { retry, share } from 'rxjs';
import { webSocket } from 'rxjs/webSocket';

export interface NotificationMessage {
  id: string;
  title: string;
  body: string;
  createdAt: string;
}

@Injectable({ providedIn: 'root' })
export class NotificationsService {
  readonly messages$ = webSocket<NotificationMessage>(
    'wss://example.com/notifications'
  ).pipe(
    retry({ delay: 1000 }),
    share()
  );
}

Si queremos mostrar la última notificación en la plantilla, podemos convertirla en señal.

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

@Component({
  selector: 'app-notification-bell',
  standalone: true,
  template: `
    @if (lastMessage(); as message) {
      <button type="button">
        🔔 {{ message.title }}
      </button>
    } @else {
      <button type="button">🔔 Nessuna notifica</button>
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class NotificationBellComponent {
  private readonly notifications = inject(NotificationsService);

  readonly lastMessage = toSignal(this.notifications.messages$, {
    initialValue: null as NotificationMessage | null
  });
}

RxJS maneja la transmisión. Signal expone el valor más reciente a la interfaz de usuario. Este es el tipo de combinación más saludable.


Interoperabilidad: toSignal y toObservable

Angular proporciona herramientas para hacer que Signals y RxJS se comuniquen entre sí. Los más comunes son toSignal y toObservable.

toSignal convierte un Observable en una señal.

readonly user = toSignal(this.authService.user$, {
  initialValue: null
});

toObservable convierte una señal a Observable.

import { signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs';

readonly query = signal('');

private readonly query$ = toObservable(this.query);

readonly results = toSignal(
  this.query$.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((query) => this.api.searchProducts(query))
  ),
  { initialValue: [] as Product[] }
);

Esta técnica es muy útil cuando la entrada del usuario se maneja como señales, pero la búsqueda debe utilizar operadores RxJS.


Errores comunes que se deben evitar

1. Reemplace RxJS en todas partes solo porque existen Signals

Las señales no eliminan RxJS. Angular continúa usando Observable en muchas API. Si intenta reemplazar cada transmisión con señal, corre el riesgo de perder a los operadores más poderosos de RxJS.

2. Utilice RxJS para cada estado local pequeño

Un booleano como isMenuOpen, una pestaña activa o un filtro local no necesitan un BehaviorSubject. Una señal es más legible.

3. Realice llamadas HTTP directamente sin verificar

effect es útil para efectos secundarios como la sincronización del almacenamiento local, el título de la página o el registro. No debería convertirse en el lugar para ocultar una lógica asincrónica compleja.

// Da evitare in scenari complessi:
effect(() => {
  const query = this.query();
  this.api.searchProducts(query).subscribe();
});

Para la eliminación de rebotes, la limpieza y el manejo de errores, una canalización RxJS es más limpia.

4. Creando demasiados cálculos inútiles

calculado es poderoso, pero no tiene por qué reemplazar cada método o cada pequeña expresión. Úselo cuando el valor derivado sea importante, reutilizado o costoso de calcular.

5. Convertir de ida y vuelta sin patrón

Si tiene un Observable usado solo en la plantilla, la tubería async puede ser suficiente. Utilice toSignal cuando realmente desee integrar ese valor en el gráfico de Señales.


Árbol de decisiones práctico

  • ¿Es este un estado local del componente? Utilice señales.
  • ¿Es este un valor derivado de otros estados? Utilice calculado.
  • ¿Es un flujo de acontecimientos a lo largo del tiempo? Utilice RxJS.
  • ¿Necesitas rebote? Utilice RxJS.
  • ¿Necesita eliminar solicitudes anteriores? Utilice RxJS con switchMap.
  • ¿Necesitas mostrar el último valor en la plantilla? Considere toSignal.
  • ¿Es esta una lista ya cargada para ser filtrada localmente? Señales.
  • ¿Es esta una búsqueda remota mientras el usuario escribe? RxJS.
  • ¿Es este un carrito con totales derivados? Señales.
  • ¿Es esto un WebSocket? RxJS.
  • ¿Es este un formulario con validación asincrónica? RxJS plus Signals si necesita UI.
  • estado
  • ¿Es una tienda de funciones sencilla? Señales.
  • ¿Depende la orquestación de múltiples llamadas HTTP? RxJS.

Arquitectura recomendada en una aplicación real

En un proyecto Angular profesional, una buena estrategia es separar la lógica en una capa data-access. Los componentes no deben saber demasiado sobre HTTP, almacenamiento en caché, reintento o mapeo de datos.

src/app/features/products/
  products.routes.ts
  pages/
    products-page.component.ts
    product-detail-page.component.ts
  components/
    product-card.component.ts
    product-filters.component.ts
  data-access/
    products-api.service.ts
    products-store.service.ts
  models/
    product.model.ts

En el archivo de servicio API puede usar RxJS porque Angular HttpClient devuelve Observable.

En la tienda puede guardar el resultado en Señales, crear filtros y totales calculados y exponer métodos simples al componente.

En el componente puede leer Señales en la plantilla, use la tubería async cuando trabaje directamente con Observable, o convierta con toSignal cuando mejore la legibilidad.


Signals y RxJS no se excluyen mutuamente. Las señales hacen que Angular sea más fácil para el estado local, el estado derivado y la interfaz de usuario. RxJS sigue siendo fundamental para transmisiones asincrónicas, transmisiones, eliminaciones, reintentos, eliminación de rebotes y composición avanzada.

La mejor opción es utilizar la herramienta adecuada en el lugar correcto. Si representa un valor actual, elija Señales. Si está modelando eventos a lo largo del tiempo, elija RxJS. Si necesitas ambos, combínalos sin forzar.


💬 Notas de los lectores

0 notas

Escribe una nota

Comparte tu opinión, una sugerencia o un cumplido

Últimas notas

Aún no hay notas. ¡Sé el primero en comentar!