Interoperabilidad signals con RxJS
La coexistencia de sistemas reactivos en Angular moderno presenta un desafío único: cómo integrar eficientemente los signals, el nuevo primitivo reactivo introducido en Angular 17, con RxJS, que ha sido la base de la programación reactiva en el framework durante años. Esta interoperabilidad no es solo una conveniencia técnica, sino una necesidad arquitectónica para migrar gradualmente aplicaciones existentes y aprovechar lo mejor de ambos mundos.
El problema de la integración reactiva
Antes de la introducción de signals, Angular dependía completamente de RxJS observables para el manejo de datos asíncronos y la reactividad. Los observables ofrecen un sistema robusto con operadores sofisticados para transformar, filtrar y combinar flujos de datos. Sin embargo, su naturaleza asíncrona y la complejidad de gestión de suscripciones pueden generar overhead tanto en rendimiento como en complejidad de código.
Los signals, por otro lado, proporcionan una reactividad síncrona y granular que se integra naturalmente con el sistema de detección de cambios de Angular. Son más simples de usar para estados síncronos y ofrecen mejor rendimiento para actualizaciones frecuentes del UI.
Escenarios de interoperabilidad
En aplicaciones reales, encontramos varios patrones de integración que requieren puentes entre estos sistemas:
Datos asíncronos en signals: Cuando recibimos datos de APIs HTTP (que devuelven observables) pero queremos manejar el estado resultante como signals para aprovechar su reactividad granular en templates.
Estado derivado mixto: Situaciones donde necesitamos crear computed signals que dependan tanto de otros signals como de streams de datos asíncronos provenientes de observables.
Migración progresiva: Aplicaciones existentes que utilizan servicios basados en RxJS pero quieren adoptar signals en componentes específicos sin reescribir toda la arquitectura de datos.
Funciones de conversión
Angular proporciona en el paquete @angular/rxjs-interop
dos funciones utilitarias fundamentales para esta interoperabilidad:
- La función
toSignal()
convierte observables en signals, permitiendo que los datos asíncronos se integren seamlessly en el ecosistema de signals. Esta conversión maneja automáticamente la suscripción y limpieza del observable, eliminando la necesidad de gestión manual de suscripciones. - La función
toObservable()
realiza la conversión inversa, transformando signals en observables. Esto permite que los signals participen en pipelines de RxJS complejos, aprovechando la riqueza de operadores disponibles.
Consideraciones de rendimiento
La interoperabilidad introduce consideraciones específicas de rendimiento y arquitectura. Cada conversión implica un overhead mínimo, por lo que es importante entender cuándo y dónde aplicar estas transformaciones.
Los signals mantienen su naturaleza síncrona incluso cuando se derivan de observables, pero el observable subyacente sigue siendo asíncrono. Esto significa que el signal reflejará el último valor emitido, proporcionando acceso síncrono a datos asíncronos.
Gestión del ciclo de vida
Un aspecto crucial de la interoperabilidad es la gestión automática del ciclo de vida. Cuando convertimos observables a signals, Angular se encarga de suscribirse al observable y limpiarlo cuando el contexto de inyección (típicamente un componente) es destruido.
Esta gestión automática elimina uno de los principales puntos de fricción al trabajar con observables: la necesidad de unsubscribe manual para evitar memory leaks. El sistema de signals hereda esta responsabilidad, manteniendo la simplicidad de uso característica de los signals.
La interoperabilidad entre signals y RxJS representa un puente arquitectónico que permite a los desarrolladores aprovechar las fortalezas de ambos sistemas reactivos, facilitando tanto la migración gradual como el desarrollo híbrido en aplicaciones Angular modernas.
La función toSignal()
La función toSignal()
es la herramienta principal para convertir observables de RxJS en signals, proporcionando un puente directo entre el mundo asíncrono de los observables y el ecosistema síncrono de signals. Esta conversión permite que los datos provenientes de servicios HTTP, rutas, o cualquier stream de RxJS se integren naturalmente en templates y lógica basada en signals.
Sintaxis básica y importación
La función toSignal()
se importa desde @angular/core/rxjs-interop
y acepta un observable como primer parámetro, devolviendo un signal que refleja el último valor emitido por ese observable:
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { Component, inject } from '@angular/core';
@Component({
selector: 'app-user-profile',
standalone: true,
template: `
@if (user()) {
<div>
<h2>{{ user().name }}</h2>
<p>{{ user().email }}</p>
</div>
} @else {
<p>Cargando usuario...</p>
}
`
})
export class UserProfileComponent {
private http = inject(HttpClient);
// Conversión directa de Observable a Signal
user = toSignal(this.http.get<User>('/api/user/123'));
}
interface User {
name: string;
email: string;
}
Configuración con opciones
La función toSignal()
acepta un segundo parámetro de configuración que permite personalizar el comportamiento de la conversión:
@Component({
selector: 'app-product-list',
standalone: true,
template: `
@if (products() === undefined) {
<div class="loading">Cargando productos...</div>
} @else if (products().length === 0) {
<div class="empty">No hay productos disponibles</div>
} @else {
@for (product of products(); track product.id) {
<div class="product-card">
<h3>{{ product.name }}</h3>
<p>{{ product.price | currency }}</p>
</div>
}
}
`
})
export class ProductListComponent {
private http = inject(HttpClient);
products = toSignal(
this.http.get<Product[]>('/api/products'),
{
initialValue: [], // Valor inicial mientras se carga
requireSync: false // Permite valores undefined inicialmente
}
);
}
Manejo de estados de carga y error
Para un control más granular sobre los estados de carga y error, podemos combinar toSignal()
con operadores de RxJS que estructuren la respuesta:
import { catchError, map, startWith } from 'rxjs/operators';
import { of } from 'rxjs';
interface DataState<T> {
loading: boolean;
data: T | null;
error: string | null;
}
@Component({
selector: 'app-dashboard',
standalone: true,
template: `
@if (dashboardState().loading) {
<div class="spinner">Cargando datos del dashboard...</div>
} @else if (dashboardState().error) {
<div class="error">
Error: {{ dashboardState().error }}
<button (click)="reloadData()">Reintentar</button>
</div>
} @else {
<div class="dashboard">
<h2>Estadísticas</h2>
<p>Ventas: {{ dashboardState().data?.sales }}</p>
<p>Usuarios activos: {{ dashboardState().data?.activeUsers }}</p>
</div>
}
`
})
export class DashboardComponent {
private http = inject(HttpClient);
dashboardState = toSignal(
this.http.get<DashboardData>('/api/dashboard').pipe(
map(data => ({ loading: false, data, error: null })),
catchError(error => of({
loading: false,
data: null,
error: error.message
})),
startWith({ loading: true, data: null, error: null })
),
{ requireSync: true }
);
reloadData() {
// Lógica para recargar los datos
window.location.reload();
}
}
Integración con parámetros de rutas
Una aplicación práctica común es convertir parámetros de rutas en signals para crear componentes reactivos que respondan automáticamente a cambios de navegación:
import { ActivatedRoute } from '@angular/router';
import { switchMap } from 'rxjs/operators';
@Component({
selector: 'app-article-detail',
standalone: true,
template: `
@if (article()) {
<article>
<h1>{{ article().title }}</h1>
<div class="meta">
<span>Por {{ article().author }}</span>
<span>{{ article().publishedAt | date }}</span>
</div>
<div class="content">{{ article().content }}</div>
</article>
} @else {
<div class="loading">Cargando artículo...</div>
}
`
})
export class ArticleDetailComponent {
private route = inject(ActivatedRoute);
private http = inject(HttpClient);
// Signal que se actualiza automáticamente cuando cambia el parámetro de ruta
article = toSignal(
this.route.params.pipe(
switchMap(params =>
this.http.get<Article>(`/api/articles/${params['id']}`)
)
)
);
}
Contexto de inyección y cleanup automático
Una ventaja clave de toSignal()
es la gestión automática de suscripciones. La función detecta automáticamente el contexto de inyección y se encarga de hacer cleanup cuando el componente es destruido:
@Component({
selector: 'app-notifications',
standalone: true,
template: `
@for (notification of notifications(); track notification.id) {
<div class="notification" [class.unread]="!notification.read">
{{ notification.message }}
</div>
}
`
})
export class NotificationsComponent {
private http = inject(HttpClient);
// No necesitamos gestión manual de suscripciones
// toSignal() se encarga del cleanup automáticamente
notifications = toSignal(
this.http.get<Notification[]>('/api/notifications').pipe(
// Polling cada 30 segundos
switchMap(data =>
timer(0, 30000).pipe(
switchMap(() => this.http.get<Notification[]>('/api/notifications'))
)
)
),
{ initialValue: [] }
);
}
Combinación con computed signals
Los signals creados con toSignal()
pueden participar en computed signals para crear derivaciones reactivas complejas:
@Component({
selector: 'app-user-dashboard',
standalone: true,
template: `
<div class="user-stats">
<h3>Resumen de {{ user()?.name }}</h3>
<p>Proyectos completados: {{ completedProjects() }}</p>
<p>Progreso general: {{ overallProgress() }}%</p>
</div>
`
})
export class UserDashboardComponent {
private http = inject(HttpClient);
user = toSignal(this.http.get<User>('/api/user'));
projects = toSignal(
this.http.get<Project[]>('/api/projects'),
{ initialValue: [] }
);
// Computed signal que deriva de signals creados con toSignal()
completedProjects = computed(() =>
this.projects().filter(p => p.status === 'completed').length
);
overallProgress = computed(() => {
const total = this.projects().length;
const completed = this.completedProjects();
return total > 0 ? Math.round((completed / total) * 100) : 0;
});
}
La función toSignal()
representa la puerta de entrada principal para integrar datos asíncronos en el ecosistema de signals, proporcionando una API simple pero flexible que mantiene la reactividad mientras elimina la complejidad de gestión manual de suscripciones.

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 la necesidad de interoperabilidad entre signals y RxJS en Angular.
- Aprender a usar la función toSignal() para convertir observables en signals.
- Configurar toSignal() para manejar estados iniciales, carga y errores.
- Integrar signals derivados de observables en componentes y templates.
- Gestionar automáticamente el ciclo de vida y suscripciones con toSignal().