# RxJS en Angular 2026: operators, Subjects e interop con Signals
> RxJS en Angular 2026: domina los operators, los Subjects y los patrones de interop con Signals que se usan en producción, además de las preguntas de entrevista más frecuentes.
- Published: 2026-07-02
- Updated: 2026-07-07
- Author: SharpSkill
- Tags: angular, rxjs, signals, observables, typescript, deep-dive
- Reading time: 10 min
---
RxJS sigue siendo la columna vertebral de la programación asíncrona en Angular en 2026, incluso cuando los Signals reconfiguran la forma en que los componentes de Angular rastrean el estado síncrono. Saber cuándo recurrir a un Observable, cuándo encaja mejor un Signal y cómo interoperan ambos es ahora una habilidad central para cualquier desarrollador Angular. Este análisis a fondo recorre los operators, los Subjects y los patrones de interop que separan una respuesta de entrevista segura de una que se atasca.
> **Signals frente a Observables en una línea**
>
> Los Signals modelan estado síncrono que siempre tiene un valor actual; los Observables modelan flujos asíncronos de eventos a lo largo del tiempo. Angular 20 conserva ambos, y el paquete `@angular/core/rxjs-interop` es el puente oficial entre ellos.
## Por qué RxJS sigue importando junto a los Angular Signals
La llegada de los Signals llevó a muchos equipos a preguntarse si RxJS estaba de salida. La respuesta, tres versiones mayores después, es no. Los Signals sobresalen al representar estado que tiene un valor ahora mismo: un contador, un campo de formulario, una pestaña seleccionada. Los Observables sobresalen al modelar eventos que llegan a lo largo del tiempo y que pueden necesitar transformación, cancelación o combinación: respuestas HTTP, mensajes WebSocket, entrada de teclado, eventos del router y temporizadores.
Las propias APIs de Angular vuelven visible esa división. `HttpClient` sigue devolviendo Observables. Los formularios reactivos exponen `valueChanges` y `statusChanges` como Observables. Los eventos del router fluyen a través de un Observable. Dondequiera que múltiples eventos asíncronos circulen por el mismo canal, los operators de RxJS siguen siendo la herramienta más expresiva disponible. La [guía oficial de reactividad de Angular](https://angular.dev/guide/signals/rxjs-interop) trata ambos modelos como complementarios en lugar de competidores, y la [documentación de RxJS](https://rxjs.dev/guide/overview) sigue siendo una referencia obligada para cualquier flujo de datos no trivial.
La regla práctica que resiste en la revisión de código: usar Signals para el estado local del componente que se lee en las plantillas, y usar Observables para los pipelines asíncronos que necesitan operators. La capa de interop conecta ambos para que ningún modelo se convierta en un muro.
Los operators de combinación refuerzan la idea. Coordinar varias fuentes asíncronas, como fusionar los parámetros de ruta con los filtros guardados de un usuario y un feed de precios en vivo, es exactamente para lo que se crearon `combineLatest`, `withLatestFrom` y `forkJoin`. Los Signals pueden derivar valores calculados a partir de otros Signals, pero no pueden expresar coordinación basada en el tiempo como "emitir solo después de que las tres fuentes hayan producido al menos un valor". Esa composición declarativa en el tiempo es la razón perdurable por la que RxJS permanece en la caja de herramientas.
## Los operators de RxJS en los que se apoya todo desarrollador Angular
Los operators son funciones puras que toman un Observable y devuelven uno nuevo. El escenario clásico de entrevista es la búsqueda mientras se escribe, porque ejercita cuatro operators a la vez: limitar la entrada, omitir duplicados, cancelar peticiones obsoletas y recuperarse de errores.
```typescript
// search.component.ts
import { Component, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
selector: 'app-search',
imports: [ReactiveFormsModule],
template: ``,
})
export class SearchComponent {
private http = inject(HttpClient);
query = new FormControl('');
results$ = this.query.valueChanges.pipe(
debounceTime(300), // wait until typing pauses for 300ms
distinctUntilChanged(), // ignore emissions that repeat the last value
switchMap((term) => // cancel the in-flight request, start a fresh one
this.http.get(`/api/search?q=${term}`).pipe(
catchError(() => of([])) // recover from a failed request without killing the stream
)
)
);
}
```
El detalle sutil que sondean los entrevistadores es por qué `switchMap` se ubica en el nivel externo mientras que `catchError` queda anidado dentro de él. Colocar `catchError` en el Observable HTTP interno significa que una petición fallida devuelve un arreglo vacío y el flujo externo sigue circulando. Mover `catchError` al pipe externo terminaría toda la búsqueda tras el primer error, rompiendo cada pulsación posterior. Los cuatro operators de aplanamiento responden cada uno a una pregunta de concurrencia distinta, y elegir el equivocado es una de las fuentes más comunes de race conditions en las aplicaciones Angular.
> **Cómo elegir un operator de aplanamiento**
>
> Usar `switchMap` para cancelar la petición interna anterior y conservar solo la más reciente (búsqueda typeahead). Usar `mergeMap` para ejecutar cada petición interna en paralelo (subidas independientes). Usar `concatMap` para encolar las peticiones en orden estricto (escrituras secuenciales que no deben solaparse). Usar `exhaustMap` para ignorar nuevas entradas hasta que la actual termine (bloquear un botón de envío pulsado dos veces).
Acertar en esta elección es lo que convierte una funcionalidad inestable en una predecible. El [módulo de operators de RxJS de SharpSkill](/technologies/angular/interview-questions/rxjs-operators) practica estas distinciones con preguntas calificadas que reproducen las race conditions exactas que cada operator previene.
## Subjects y multicasting: BehaviorSubject, ReplaySubject y Subject
Un Observable simple es unicast: cada suscriptor dispara una ejecución nueva. Un Subject es a la vez un Observable y un Observer, lo que lo vuelve multicast. Esa propiedad convierte a los Subjects en la forma estándar de construir un store de estado ligero o un bus de eventos dentro de un servicio.
Las tres variantes difieren en lo que repiten a los suscriptores tardíos. Un `Subject` simple no emite nada del pasado, así que un suscriptor solo ve los valores que llegan después de suscribirse. Un `BehaviorSubject` almacena el valor actual y lo repite de inmediato, razón por la cual necesita un valor inicial. Un `ReplaySubject` almacena en búfer un número configurable de emisiones pasadas y las repite todas.
```typescript
// cart.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class CartService {
// BehaviorSubject holds the latest value and replays it to new subscribers
private readonly itemsSubject = new BehaviorSubject([]);
// expose a read-only stream, never the writable Subject itself
readonly items$ = this.itemsSubject.asObservable();
add(item: string): void {
const current = this.itemsSubject.value; // synchronous access to the last value
this.itemsSubject.next([...current, item]); // emit the next immutable state
}
}
```
Dos hábitos importan aquí. Primero, exponer `asObservable()` en lugar del Subject impide que el código externo llame a `next()` y mute el estado por la puerta trasera. Segundo, emitir una nueva referencia de arreglo en lugar de mutar en el sitio mantiene predecibles la detección de cambios y las verificaciones de igualdad. Este patrón antecede a los Signals y sigue funcionando bien para estado compartido e inyectable que consumen varios componentes sin relación entre sí.
Dicho esto, un `BehaviorSubject` usado únicamente para sostener un solo valor actual leído en las plantillas a menudo se expresa hoy mejor como un Signal escribible, que elimina la suscripción y el ceremonial de `asObservable()`. El Subject sigue siendo la mejor opción cuando el store también necesita transformar, aplicar debounce o combinar sus emisiones con otros flujos, ya que esos operators no tienen equivalente directo del lado de los Signals. Decidir entre ambos según cada caso de uso, en lugar de imponer uno por defecto en todas partes, es una marca de fluidez que los entrevistadores premian.
## Interop de RxJS con Signals mediante toSignal y toObservable
El paquete de interop trae dos funciones estelares. `toSignal` convierte un Observable en un Signal de solo lectura que una plantilla puede consumir directamente, y gestiona el ciclo de vida de la suscripción de forma automática. `toObservable` hace lo inverso, convirtiendo un Signal en un Observable para que los operators de RxJS puedan procesar sus cambios.
```typescript
// dashboard.component.ts
import { Component, signal, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { switchMap } from 'rxjs/operators';
@Component({ selector: 'app-dashboard', template: `{{ user()?.name }}` })
export class DashboardComponent {
private http = inject(HttpClient);
// a Signal drives the query; toObservable turns each change into a stream event
readonly userId = signal(1);
private readonly user$ = toObservable(this.userId).pipe(
switchMap((id) => this.http.get<{ name: string }>(`/api/users/${id}`))
);
// toSignal subscribes immediately and unsubscribes on destroy, no manual teardown
readonly user = toSignal(this.user$, { initialValue: null });
}
```
La opción `initialValue` merece atención. Sin ella, `toSignal` devuelve un Signal tipado como el valor o `undefined`, porque todavía nada ha emitido. Pasar `initialValue: null` le da a la plantilla un estado de partida definido y un tipo más limpio. Para fuentes síncronas como un `BehaviorSubject`, `requireSync: true` garantiza que haya un valor disponible en la primera lectura y elimina `undefined` del tipo por completo. Este puente bidireccional es lo que permite a una base de código adoptar los Signals de forma incremental sin reescribir cada pipeline Observable existente. El [módulo de entrevista de Angular Signals](/technologies/angular/interview-questions/angular-signals) cubre el modelo de reactividad en profundidad, y el [resumen de funcionalidades de Signals de Angular 18](/blog/angular/angular-18-signals-new-features) traza cómo evolucionaron estas primitivas.
## Conectar Observables con recursos signal usando rxResource
Angular 20 impulsa `rxResource` como la forma declarativa de cargar datos asíncronos desde un Observable hacia estado basado en signals. Rastrea el estado de carga, error y resolución como signals, lo que elimina la mayor parte del control manual de suscripciones que antes exigían las llamadas HTTP.
```typescript
// products.component.ts
import { Component, signal, inject } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
@Component({ selector: 'app-products', template: `` })
export class ProductsComponent {
private http = inject(HttpClient);
readonly category = signal('laptops');
// Angular 20 renamed the fields: request -> params and loader -> stream
readonly products = rxResource({
params: () => ({ category: this.category() }),
stream: ({ params }) =>
this.http.get(`/api/products?category=${params.category}`),
});
// products.value(), products.isLoading() and products.error() are all signals
}
```
El cambio de nombre de `request`/`loader` en Angular 19 a `params`/`stream` en Angular 20 es un tropiezo frecuente, así que mencionar los nombres de campo actuales indica conocimiento actualizado en una entrevista. Cuando el signal `category` cambia, `rxResource` cancela la petición anterior e inicia un nuevo stream, otorgando la semántica de `switchMap` sin costo. La [referencia de la API de rxResource](https://v20.angular.dev/api/core/rxjs-interop/rxResource) documenta todo el ciclo de vida de los estados.
## Evitar fugas de memoria con takeUntilDestroyed
Cualquier `subscribe()` manual que sobreviva a su componente filtra memoria y puede disparar callbacks contra una vista destruida. La solución histórica era el patrón `takeUntil(this.destroy$)` con un Subject completado en `ngOnDestroy`. Angular reemplazó ese boilerplate con `takeUntilDestroyed`, un operator que ata la suscripción al contexto de inyección actual o a un `DestroyRef` explícito.
```typescript
// live-feed.component.ts
import { Component, inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({ selector: 'app-live-feed', template: `` })
export class LiveFeedComponent {
private destroyRef = inject(DestroyRef);
constructor() {
interval(1000)
// completes the subscription automatically when the component is destroyed
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((tick) => console.log('tick', tick));
}
}
```
Llamado dentro de un constructor o de un inicializador de campo, `takeUntilDestroyed()` no necesita argumento porque lee el `DestroyRef` ambiental. Llamado en cualquier otro lugar, como dentro de un método, exige un `DestroyRef` explícito pasado como argumento, según se muestra arriba. Preferir `toSignal` y `rxResource` evita las suscripciones manuales por completo, que es la manera más limpia de esquivar las fugas; cuando de verdad se necesita una suscripción cruda, `takeUntilDestroyed` es la protección correcta.
> **La trampa del contexto de inyección**
>
> Llamar a `takeUntilDestroyed()` sin argumento fuera de un contexto de inyección lanza un error en tiempo de ejecución. Suscribirse dentro de un manejador de eventos, un `setTimeout` o un callback de RxJS queda fuera de ese contexto, así que el `DestroyRef` debe capturarse antes (por lo general como un campo inyectado) y pasarse explícitamente. Olvidar esto es la causa más frecuente del error "takeUntilDestroyed() can only be used within an injection context".
## Preguntas de entrevista de Angular RxJS que vale la pena ensayar
Los entrevistadores tienden a girar en torno a los mismos temas de alto valor. Responderlos con claridad separa a los candidatos que han usado RxJS de los que solo han leído sobre él.
- **Observables calientes frente a fríos.** Un Observable frío arranca su productor por suscripción, así que cada suscriptor obtiene una ejecución independiente (una nueva petición HTTP). Un Observable caliente comparte un solo productor entre los suscriptores, que es lo que crean los Subjects y `share()`.
- **switchMap frente a mergeMap frente a concatMap frente a exhaustMap.** `switchMap` cancela el Observable interno anterior (ideal para búsqueda). `mergeMap` ejecuta todos los Observables internos en paralelo. `concatMap` los encola en orden. `exhaustMap` ignora nuevas entradas mientras hay una en curso (ideal para prevenir envíos dobles de formulario).
- **Cómo prevenir fugas de memoria.** Preferir `toSignal`, `rxResource` o el pipe `async`, que se desuscriben todos de forma automática; para suscripciones manuales, usar `takeUntilDestroyed`.
- **Cuándo convertir un Observable en un Signal.** Recurrir a `toSignal` cuando una plantilla necesita un valor de forma síncrona y la fuente es asíncrona, dejando que la detección de cambios lea el último valor sin un pipe `async`.
Trabajar versiones calificadas de estas preguntas bajo presión de tiempo es la vía más rápida para interiorizarlas. El [hub de tecnología Angular](/technologies/angular) agrupa los módulos de RxJS, Signals y detección de cambios que se corresponden directamente con estas preguntas.
## Conclusión
RxJS y los Signals son socios en el stack moderno de Angular, no rivales. Las conclusiones que vale la pena llevar al próximo proyecto o entrevista:
- Usar Signals para el estado síncrono del componente y Observables para los pipelines de eventos asíncronos que necesitan operators.
- Recurrir a `switchMap` para cancelar trabajo obsoleto, a `exhaustMap` para bloquear duplicados, y mantener `catchError` en el Observable interno para que el flujo externo sobreviva a los fallos.
- Exponer el estado de un Subject a través de `asObservable()` y emitir valores inmutables para mantener predecible la detección de cambios.
- Unir ambos modelos con `toSignal` y `toObservable`, y pasar `initialValue` o `requireSync` para obtener tipos limpios.
- Cargar datos asíncronos de forma declarativa con `rxResource` en Angular 20, recordando los nombres de campo `params` y `stream`.
- Proteger cada suscripción manual restante con `takeUntilDestroyed` para eliminar las fugas.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/es/blog/angular/rxjs-angular-operators-subjects-signals-interop