toObservable(): Conversión básica
La función toObservable() representa el puente inverso entre el ecosistema de Signals y RxJS. Mientras que toSignal()
convierte Observables en Signals, toObservable()
nos permite transformar un Signal en un Observable, facilitando la integración con código RxJS existente.
Esta función es especialmente útil cuando trabajamos con APIs que esperan Observables o cuando necesitamos aplicar operadores RxJS a datos que provienen de Signals. La conversión mantiene la reactividad, emitiendo un nuevo valor cada vez que el Signal cambia.
Importación y sintaxis básica
Para utilizar toObservable()
, debemos importarla desde @angular/core/rxjs-interop
:
import { toObservable } from '@angular/core/rxjs-interop';
import { signal } from '@angular/core';
La sintaxis básica es directa: pasamos un Signal como argumento y obtenemos un Observable que emite los valores del Signal:
const contador = signal(0);
const contador$ = toObservable(contador);
// El Observable emite cada vez que el Signal cambia
contador$.subscribe(valor => console.log('Contador:', valor));
Conversión de Signals básicos
Los Signals escritos se convierten sin complicaciones. Cada llamada a set()
o update()
del Signal original dispara una emisión en el Observable resultante:
const nombre = signal('Juan');
const nombre$ = toObservable(nombre);
nombre$.subscribe(valor => console.log('Nombre actual:', valor));
// Cambiar el Signal emite en el Observable
nombre.set('María'); // Console: "Nombre actual: María"
nombre.set('Carlos'); // Console: "Nombre actual: Carlos"
Los computed Signals también se convierten correctamente, emitiendo cuando cualquiera de sus dependencias cambia:
const precio = signal(100);
const descuento = signal(10);
const precioFinal = computed(() => {
return precio() * (1 - descuento() / 100);
});
const precioFinal$ = toObservable(precioFinal);
precioFinal$.subscribe(valor => {
console.log('Precio final:', valor);
});
// Cambiar cualquier dependencia emite en el Observable
precio.set(200); // Console: "Precio final: 180"
descuento.set(20); // Console: "Precio final: 160"
Uso en componentes
En el contexto de componentes, toObservable()
debe ejecutarse dentro del injection context. Esto significa que generalmente lo utilizaremos en el constructor, en inject()
o en métodos que tengan acceso al contexto de inyección:
@Component({
selector: 'app-usuario',
template: `
<h3>Usuario: {{ nombre() }}</h3>
<p>Estado: {{ estado() }}</p>
`,
standalone: true
})
export class UsuarioComponent implements OnInit {
private http = inject(HttpClient);
nombre = signal('');
estado = signal('Cargando...');
ngOnInit() {
const nombre$ = toObservable(this.nombre);
// Usar el Observable para hacer peticiones HTTP
nombre$.pipe(
filter(nombre => nombre.length > 0),
switchMap(nombre => this.http.get(`/api/usuarios/${nombre}`))
).subscribe(usuario => {
this.estado.set('Cargado');
});
}
cambiarUsuario(nuevoNombre: string) {
this.nombre.set(nuevoNombre);
}
}
Características del Observable resultante
El Observable generado por toObservable()
tiene comportamientos específicos que debemos conocer:
- Emisión inicial: El Observable emite inmediatamente el valor actual del Signal al suscribirse
- Emisiones posteriores: Solo emite cuando el Signal realmente cambia de valor
- Detección de cambios: Utiliza la igualdad referencial por defecto para determinar cambios
const datos = signal({ id: 1, nombre: 'Producto A' });
const datos$ = toObservable(datos);
datos$.subscribe(valor => console.log('Datos:', valor));
// Console: "Datos: { id: 1, nombre: 'Producto A' }"
// Mismo objeto, no emite
datos.set({ id: 1, nombre: 'Producto A' }); // Sin emisión
// Objeto diferente, sí emite
datos.set({ id: 2, nombre: 'Producto B' }); // Console: "Datos: { id: 2, nombre: 'Producto B' }"
Casos de uso prácticos
La conversión es especialmente valiosa en escenarios donde necesitamos conectar Signals con APIs existentes que esperan Observables:
@Component({
selector: 'app-buscador',
template: `
<input (input)="termino.set($event.target.value)" placeholder="Buscar...">
@if (resultados().length > 0) {
<ul>
@for (item of resultados(); track item.id) {
<li>{{ item.nombre }}</li>
}
</ul>
}
`,
standalone: true
})
export class BuscadorComponent implements OnInit {
private http = inject(HttpClient);
termino = signal('');
resultados = signal<any[]>([]);
ngOnInit() {
const termino$ = toObservable(this.termino);
termino$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(termino => {
if (termino.trim()) {
return this.http.get<any[]>(`/api/buscar?q=${termino}`);
}
return of([]);
})
).subscribe(resultados => {
this.resultados.set(resultados);
});
}
}
Esta aproximación nos permite aprovechar la simplicidad de los Signals para el estado local mientras utilizamos la flexibilidad de RxJS para operaciones complejas como debouncing y manejo de peticiones HTTP.
toObservable(): integración con operadores RxJS
La verdadera utilidad de toObservable()
se manifiesta cuando necesitamos aplicar operadores RxJS a los datos provenientes de Signals. Esta integración nos permite combinar la simplicidad de los Signals con el ecosistema completo de operadores reactivos, creando flujos de datos sofisticados y optimizados.
Operadores de transformación
Los operadores de transformación como map
, switchMap
y mergeMap
encuentran su aplicación natural cuando trabajamos con Signals que contienen identificadores o parámetros que necesitan ser transformados en peticiones HTTP:
@Component({
selector: 'app-detalle-producto',
template: `
@if (producto(); as prod) {
<h2>{{ prod.nombre }}</h2>
<p>{{ prod.descripcion }}</p>
<p>Precio: {{ prod.precio | currency }}</p>
}
@if (cargando()) {
<p>Cargando producto...</p>
}
`,
standalone: true
})
export class DetalleProductoComponent implements OnInit {
private http = inject(HttpClient);
productoId = signal<number | null>(null);
producto = signal<any>(null);
cargando = signal(false);
ngOnInit() {
const productoId$ = toObservable(this.productoId);
productoId$.pipe(
filter(id => id !== null),
tap(() => this.cargando.set(true)),
switchMap(id => this.http.get(`/api/productos/${id}`)),
tap(() => this.cargando.set(false))
).subscribe(producto => {
this.producto.set(producto);
});
}
cargarProducto(id: number) {
this.productoId.set(id);
}
}
Operadores de combinación
Los operadores de combinación como combineLatest
, merge
y zip
permiten crear flujos complejos que reaccionan a múltiples Signals simultáneamente:
@Component({
selector: 'app-dashboard-ventas',
template: `
<div>
<select (change)="periodo.set($event.target.value)">
<option value="semana">Última semana</option>
<option value="mes">Último mes</option>
<option value="año">Último año</option>
</select>
<select (change)="categoria.set($event.target.value)">
<option value="">Todas las categorías</option>
<option value="electronica">Electrónica</option>
<option value="ropa">Ropa</option>
</select>
</div>
@if (estadisticas(); as stats) {
<div class="estadisticas">
<h3>Ventas: {{ stats.total | currency }}</h3>
<p>Productos vendidos: {{ stats.cantidad }}</p>
</div>
}
`,
standalone: true
})
export class DashboardVentasComponent implements OnInit {
private http = inject(HttpClient);
periodo = signal('mes');
categoria = signal('');
estadisticas = signal<any>(null);
ngOnInit() {
const periodo$ = toObservable(this.periodo);
const categoria$ = toObservable(this.categoria);
combineLatest([periodo$, categoria$]).pipe(
debounceTime(300),
switchMap(([periodo, categoria]) => {
const params = new HttpParams()
.set('periodo', periodo)
.set('categoria', categoria);
return this.http.get('/api/estadisticas/ventas', { params });
}),
catchError(error => {
console.error('Error al cargar estadísticas:', error);
return of(null);
})
).subscribe(estadisticas => {
this.estadisticas.set(estadisticas);
});
}
}
Operadores de control de flujo
Los operadores de control de flujo como debounceTime
, throttleTime
y distinctUntilChanged
son especialmente útiles para optimizar la frecuencia de actualizaciones cuando los Signals cambian rápidamente:
@Component({
selector: 'app-configuracion-tiempo-real',
template: `
<div>
<label>
Intervalo de actualización (ms):
<input type="range"
min="100"
max="5000"
[value]="intervalo()"
(input)="intervalo.set(+$event.target.value)">
<span>{{ intervalo() }}ms</span>
</label>
<label>
<input type="checkbox"
[checked]="activo()"
(change)="activo.set($event.target.checked)">
Monitoreo activo
</label>
</div>
@if (datos(); as datosActuales) {
<div class="metricas">
<p>CPU: {{ datosActuales.cpu }}%</p>
<p>Memoria: {{ datosActuales.memoria }}%</p>
<p>Última actualización: {{ datosActuales.timestamp | date:'HH:mm:ss' }}</p>
</div>
}
`,
standalone: true
})
export class ConfiguracionTiempoRealComponent implements OnInit, OnDestroy {
private http = inject(HttpClient);
intervalo = signal(1000);
activo = signal(true);
datos = signal<any>(null);
private subscription?: Subscription;
ngOnInit() {
const intervalo$ = toObservable(this.intervalo);
const activo$ = toObservable(this.activo);
this.subscription = combineLatest([intervalo$, activo$]).pipe(
distinctUntilChanged(),
switchMap(([intervalo, activo]) => {
if (!activo) {
return EMPTY;
}
return timer(0, intervalo).pipe(
switchMap(() => this.http.get('/api/metricas/sistema'))
);
}),
retry({
count: 3,
delay: 2000
})
).subscribe(datos => {
this.datos.set(datos);
});
}
ngOnDestroy() {
this.subscription?.unsubscribe();
}
}
Patrones avanzados con múltiples Signals
Cuando trabajamos con múltiples Signals relacionados, podemos crear patrones sofisticados que reaccionan a cambios complejos en el estado:
@Component({
selector: 'app-lista-filtrada',
template: `
<div class="controles">
<input (input)="filtroTexto.set($event.target.value)"
placeholder="Filtrar por nombre...">
<select (change)="ordenPor.set($event.target.value)">
<option value="nombre">Ordenar por nombre</option>
<option value="fecha">Ordenar por fecha</option>
<option value="precio">Ordenar por precio</option>
</select>
<label>
<input type="checkbox"
[checked]="soloActivos()"
(change)="soloActivos.set($event.target.checked)">
Solo activos
</label>
</div>
@if (elementosFiltrados().length > 0) {
<ul>
@for (elemento of elementosFiltrados(); track elemento.id) {
<li [class.inactivo]="!elemento.activo">
{{ elemento.nombre }} - {{ elemento.precio | currency }}
</li>
}
</ul>
} @else {
<p>No se encontraron elementos que coincidan con los filtros.</p>
}
`,
standalone: true
})
export class ListaFiltradaComponent implements OnInit {
elementos = signal<any[]>([]);
filtroTexto = signal('');
ordenPor = signal('nombre');
soloActivos = signal(false);
elementosFiltrados = signal<any[]>([]);
ngOnInit() {
const elementos$ = toObservable(this.elementos);
const filtroTexto$ = toObservable(this.filtroTexto);
const ordenPor$ = toObservable(this.ordenPor);
const soloActivos$ = toObservable(this.soloActivos);
combineLatest([elementos$, filtroTexto$, ordenPor$, soloActivos$]).pipe(
debounceTime(200),
map(([elementos, filtro, orden, activos]) => {
let resultado = [...elementos];
// Aplicar filtro de texto
if (filtro.trim()) {
const filtroLower = filtro.toLowerCase();
resultado = resultado.filter(elem =>
elem.nombre.toLowerCase().includes(filtroLower)
);
}
// Aplicar filtro de activos
if (activos) {
resultado = resultado.filter(elem => elem.activo);
}
// Aplicar ordenación
resultado.sort((a, b) => {
switch (orden) {
case 'fecha':
return new Date(b.fecha).getTime() - new Date(a.fecha).getTime();
case 'precio':
return b.precio - a.precio;
default:
return a.nombre.localeCompare(b.nombre);
}
});
return resultado;
})
).subscribe(elementosFiltrados => {
this.elementosFiltrados.set(elementosFiltrados);
});
}
}
Gestión de errores y reintentos
La integración con operadores RxJS también nos permite implementar estrategias robustas de manejo de errores cuando los Signals disparan operaciones que pueden fallar:
@Injectable({
providedIn: 'root'
})
export class SincronizadorService {
private http = inject(HttpClient);
configuracion = signal({
url: '',
intervalo: 5000,
activo: false
});
ultimoEstado = signal<'sincronizado' | 'error' | 'desconectado'>('desconectado');
iniciarSincronizacion() {
const configuracion$ = toObservable(this.configuracion);
return configuracion$.pipe(
distinctUntilChanged((prev, curr) =>
prev.url === curr.url &&
prev.intervalo === curr.intervalo &&
prev.activo === curr.activo
),
switchMap(config => {
if (!config.activo || !config.url) {
this.ultimoEstado.set('desconectado');
return EMPTY;
}
return timer(0, config.intervalo).pipe(
switchMap(() => this.http.post(config.url, { timestamp: Date.now() })),
tap(() => this.ultimoEstado.set('sincronizado')),
retryWhen(errors =>
errors.pipe(
tap(() => this.ultimoEstado.set('error')),
delayWhen(() => timer(2000)),
take(3),
concat(throwError(() => new Error('Máximo de reintentos alcanzado')))
)
)
);
})
);
}
}
Esta integración entre toObservable() y los operadores RxJS nos proporciona lo mejor de ambos mundos: la simplicidad y eficiencia de los Signals para el estado local, combinada con la flexibilidad y robustez de RxJS para operaciones complejas y manejo de flujos de datos asincrónicos.

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 función toObservable() y su uso para convertir Signals en Observables.
- Aprender a integrar Signals con operadores RxJS para manipular flujos de datos.
- Conocer cómo aplicar toObservable() en componentes Angular dentro del contexto de inyección.
- Identificar patrones avanzados de combinación y control de flujo con múltiples Signals.
- Implementar estrategias de manejo de errores y reintentos usando RxJS junto con Signals.