Menú de navegaciónMenú
Categorías
Logo campusMVP.es

La mejor forma de Aprender Programación online y en español www.campusmvp.es

Observables Nativos vs RxJS: las diferencias clave y su impacto en Angular

Imagen Ornamental

Los Observables Nativos han llegado al navegador, y esto es un gran paso adelante para el desarrollo web. De momento solo están disponibles en Chrome (y navegadores basados en Chromium), pero se espera que lleguen pronto a Firefox y Safari.

Los Observables Nativos no deben confundirse con los Observables de RxJS, que son los que la mayoría conocemos y usamos habitualmente. Aunque comparten el nombre, tienen diferencias importantes en su estructura y comportamiento, como vas a descubrir hoy.

El concepto de Observable en programación Web

Un Observable es un mecanismo que permite manejar flujos de datos o eventos a lo largo del tiempo. Puedes pensar en un Observable como una secuencia de valores que se emiten de forma asíncrona para que alguien los recoja.

Para empezar a escuchar los valores que emite un Observable, te "suscribes" a él. Cuando te suscribes, el Observable puede comenzar a ejecutar y emitir sus valores.

Los Observables son útiles para modelar disparadores de eventos (como los eventos del DOM), o para manejar la finalización de tareas asíncronas.

Por ejemplo, normalmente para registrar manejadores para un evento del DOM usamos addEventListener(). Ahora, con los Observables Nativos, estos mismos elementos exponen un método when() que devuelve un Observable.

O sea: en lugar de hacer el clásico:

 document.addEventListener('mousemove', console.log);

Con los Observables nativos puedes escribir:

document.when('mousemove').subscribe(console.log);

En este caso, document.when('mousemove') crea un Observable que emitirá un valor cada vez que ocurre un evento mousemove. Al usar .subscribe(console.log), te estás "suscribiendo" a ese Observable, lo que significa que la función console.log se ejecutará cada vez que se emita uno de estos eventos mousemove.

Parece un cambio pequeño, casi de "azúcar sintáctico", pero es algo que simplifica muchas cosas y que deberías tener muy en cuenta si trabajas con JavaScript y el DOM.

En Angular se utiliza la biblioteca RxJS para disponer de Observables, lo cual simplifica mucho el código y permite gestionar los datos y sus cambios de manera sencilla. Si trabajas con Angular debes conocer este concepto. Pero, como veremos, los Observables Nativos presentan ciertas diferencias con los Observables de RxJS que es importante conocer.

Diferencias de uso de los Observables nativos con RxJS

Los Observables Nativos tienen algunas diferencias importantes con los de RxJS, y entenderlas es muy importante para utilizarlos correctamente. Especialmente de cara al futuro próximo, en el que formarán parte de todos los navegadores y empezarán a sustituir a RxJS.

Comportamiento, operadores y gestión de suscripciones

La primera gran diferencia es su comportamiento por defecto: los Observables Nativos son multicasting.

Esto significa que solo se activan con el primer suscriptor, y no reproducen valores anteriores. Piensa en ello como usar el operador share() de RxJS, en lugar de shareReplay() que es lo que se usa por defecto en esta biblioteca.

Por ejemplo, si ejecuto el siguiente código con un Observable de RxJS (fíjate en el primer import):

import { Observable } from 'rxjs';

const numeros$ = new Observable((suscriptor) => {
  suscriptor.next(1);
  suscriptor.next(2);
  setTimeout(() => {
    suscriptor.next(3);
  });
});
numeros$.subscribe((n) => console.log(`Suscriptor 1: ${n}`));
numeros$.subscribe((n) => console.log(`Suscriptor 2: ${n}`));

Verás que, en el momento de suscribirse el primer manejador, este recibe las dos primeras emisiones (el 1 y el 2) de manera síncrona. Acto seguido se suscribe el segundo manejador y también recibe los mismos primeros datos síncronos. Posteriormente, cuando se lanza de manera asíncrona pero inmediata (con el setTimeOut sin parámetro de tiempo) el tercer valor, lo reciben ambos suscriptores también de manera asíncrona.

Por sencillez ejecútalo, como he hecho yo, en algún "playground" para RxJS como por ejemplo el de PlayCode, y verás el resultado que te he indicado:

El resultado de ejecutar el observable con RxJS

Pero, si simplemente comento la primera línea para que no use RxJS sino los Observables Nativos (ejecutando el playground en Chrome), el resultado que veremos es muy diferente:

Ahora solo se recibe en los dos suscriptores el 3

En este caso se reciben los dos primeros valores síncronos en el primer suscriptor, que es el que lo activa, pero el asíncrono lo reciben ambos.

Para conseguir el mismo comportamiento con RxJS tendríamos que haber usado un Observable compartido, declarándolo así:

const numerosCompartidos$ = numeros$.pipe(share());

Y, de esta manera, se comportaría del mismo modo que un Observable Nativo, como puedes ver en la siguiente captura, en el que el código de inicialización síncrono solo se ejecuta una vez:

El mismo resultado pero con un Observable RxJS compartido

Otra diferencia importante está en cómo se usan los operadores. Mientras que en RxJS moderno usas pipe() para encadenar operadores como map() o filter(), los Observables Nativos vuelven a un estilo más directo. Los operadores en este caso son métodos que se aplican directamente sobre la instancia del Observable, pudiendo encadenarlos, como en las llamadas "interfaces fluidas".

Así, en lugar de, por ejemplo, hacer esto con un pipe, que sería lo que haces en RxJS:

numbers$.pipe(map((n) => n * 2), filter((n) => n < 10)).subscribe(console.log);

con los Observables Nativos harías simplemente:

numbers$.map((n) => n * 2).filter((n) => n < 10).subscribe(console.log);

Incluso tiene un equivalente al método tap() de RxJS, que en el caso de los nativos se llama inspect(). Este método permite examinar el flujo de datos sin alterarlos, por ejemplo simplemente para mostrarlos por consola o hacer algún tipo de depuración.

Estas diferencias en el comportamiento y la sintaxis de los operadores hacen que la transición entre uno y otro requiera cierta adaptación, pero en general son muy parecidos.

Promesas y cancelación: nuevas formas de interactuar con Observables Nativos

Con los Observables Nativos, algunas cosas que conocías de RxJS cambian para bien, sobre todo en la interacción con el código asíncrono. Ahora, los operadores como first(), last(), reduce() y forEach() devuelven directamente Promesas de JavaScript, no Observables. Esto simplifica mucho su uso dentro de flujos async/await.

En RxJS necesitabas funciones de utilidad como lastValueFrom() para convertir un Observable en una Promesa. Con los Observables Nativos, simplemente llamas a last() y ya tienes tu Promise. Esto facilita mucho el uso de async/await para trabajar con código asíncrono.

const contador = new Observable((suscriptor) => {
  // Inicializamos un contador
  let contador = 1;

  // Creamos un intervalo que emitirá números periódicamente
  const idIntervalo = setInterval(() => {
    // Emitimos el valor actual del contador y luego lo incrementamos
    suscriptor.next(contador++);
    // Si el contador supera 5, limpiamos el intervalo y completamos el Observable
    if (contador > 5) {
      clearInterval(idIntervalo);
      suscriptor.complete(); // Señalamos que no habrá más valores
    }
  });
});

// Usamos inspect() (equivalente a tap() en RxJS) para ver los valores emitidos
// y luego last() que, en Observables Nativos, devuelve una Promesa.
// await espera a que la Promesa se resuelva con el último valor y facilita el uso.
await contador.inspect(console.log).last();
console.log("finalizado");

Este ejemplo muestra cómo los Observables Nativos están diseñados para ser una parte más integrada de las Web APIs, con un enfoque en la simplicidad para escenarios comunes de async/await.

La gestión de la cancelación de suscriptores también es diferente y se alinea también más con las Web APIs modernas. En lugar de usar unsubscribe() como en RxJS, los Observables Nativos utilizan AbortController. Este es el mismo patrón que se usa para cancelar peticiones fetch().

Por ejemplo, en este fragmento con RxJS la primera llamada a unsubscribe fallaría:

//RxJS

const numeros$ = new Observable<number>((suscriptor) => {
  suscriptor.next(1);
  suscriptor.next(2);
});

const suscripcion = numeros$.subscribe((valor) => {
  console.log(valor);
  suscripcion.unsubscribe() //Fallaría
});

suscripcion.unsubscribe(); //Funciona

Con los nativos la cosa cambia:

//Nativos

const numeros$ = new Observable((suscriptor) => {
  suscriptor.next(1);
  suscriptor.next(2);
});

const abortController = new AbortController();

numeros$.subscribe(
  (valor) => {
    console.log(valor);
    if (valor >= 1) {
      console.log("Cancelando la suscripción");
      abortController.abort();
    }
  },
  { signal: abortController.signal },
);

Esto no es código Angular, sino código nativo JavaScript en el navegador. Como segundo parámetro del método subscribe se puede pasar un objeto con la propiedad signal que recibe una referencia al objeto de tipo AbortSignal obtenido por el controlador de cancelación. Cuando se llama al método abortController.abort(), se activa el AbortSignal asociado y el Observable puede saber que se ha cancelado la suscripción y dejar de notificar los valores.

Es un enfoque más integrado y que permite la cancelación síncrona, algo que RxJS no permite, ya que espera a que todas las emisiones asíncronas pendientes se procesen antes de poder cancelar.

Por último, el mecanismo de teardown (la lógica que se ejecuta cuando un Observable se completa o se cancela) también se modifica. En el constructor de un Observable de RxJS, devolvías una función para esta tarea:

//RxJS

const numeros$ = new Observable<number>((suscriptor) => {
  suscriptor.next(1);
  suscriptor.next(2);

  return () => console.log('Observable completado');
});

Ahora, con los Observables Nativos, registras esa lógica con un método addTeardown() en cada suscriptor:

//Observables nativos

const numeros$ = new Observable((suscriptor) => {
  suscriptor.addTeardown(() => {
    console.log("Observable completado");
    suscriptor.complete();
  });

  suscriptor.next(1);
  suscriptor.next(2);
});

Al tener un método dedicado para registrar la lógica de limpieza, es más evidente cuál es el propósito de esa parte del código, en lugar de depender del valor de retorno del constructor del Observable como pasa en RxJS. Además esto está más alineado con cómo se hacen las cosas en las APIs Web modernas.

Impacto en RxJS y Angular + Signals

Ahora que los Observables Nativos ya están disponibles en Chrome te preguntarás qué pasa con tecnologías como RxJS y Angular Signals. Pues bien, la buena noticia es que el desarrollo de RxJS, que estaba en pausa esperando la llegada de estos Observables nativos, ya ha retomado su curso. RxJS avanzará para integrarlos, incluyendo shims para aquellos entornos donde aún no estén disponibles. Esto significa que, aunque haya nuevas opciones nativas, RxJS seguirá siendo una herramienta relevante y se adaptará a esta nueva realidad.

Y, ¿qué hay de Angular Signals? Aquí la respuesta es sencilla: no hay cambios. Se usan para cosas diferentes. Signals se enfoca en la gestión de estados, mientras que los Observables, tanto los nativos como los de RxJS, son ideales para manejar eventos y secuencias. Los Observables seguirán siendo muy útiles para modelar disparadores de todo tipo de eventos. En Angular, la gestión de estados se está moviendo hacia Signals. En resumen: Observables y Signals tienen roles diferentes y complementarios en el ecosistema de desarrollo Angular.

Puedes consultar la documentación de los Observables Nativos para conocer más detalles de su funcionamiento. Y puedes apuntarte a nuestro curso de Angular para aprender este framewrok desde cero.

José Manuel Alarcón Fundador de campusMVP.es, es ingeniero industrial y especialista en consultoría de empresa. Ha escrito diversos libros, habiendo publicado hasta la fecha cientos de artículos sobre informática e ingeniería en publicaciones especializadas. Microsoft lo ha reconocido como MVP (Most Valuable Professional) en desarrollo web desde el año 2004 hasta la actualidad. Puedes seguirlo en LinkedIn. Ver todos los posts de José Manuel Alarcón
Archivado en: Desarrollo Web

Boletín campusMVP.es

Solo cosas útiles. Una vez al mes.

🚀 Únete a miles de desarrolladores

DATE DE ALTA

x No me interesa | x Ya soy suscriptor

La mejor formación online para desarrolladores como tú

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.