RxJS es una librería muy útil de Javascript, que te ayuda a gestionar secuencias de eventos.
Imagina una caja de búsqueda: Necesitas gestionar eventos del input, estrategias de debounce, llamadas a servidor… Ese sería un escenario ideal para RxJS.
Si nunca has oído hablar de ella, te recomiendo que le des un vistazo a mi introducción a RxJS y a mis fundamentos de RxJS. Te puede parecer muy teórico, pero eso va a cambiar a partir de YA… En este artículo voy a explicarte como se utiliza RxJS de forma práctica, a través de código.
Elementos básicos de RxJS
Déjame repasar la jerga. Los conceptos principales de RxJS son:
- Observable: El flujo de datos, una colección de eventos que se pueden emitir en algún momento.
-
Observer: Un objeto que escucha el flujo de datos y puede actuar sobre los valores que éste emite.
-
Subscription: Representa la ejecución de un observable y permite cancelarla.
-
Operador: Función para manipular los eventos siguiendo los principios de la programación funcional.
Creando un Observable
Pongamos que quiero emitir el texto «Hello world», letra a letra.
Para eso están los Observables, ¿no?
Una de las formas más simples de crear un Observable es la función from
: recibe un array o una cadena de texto y devuelve un flujo con su contenido, item a item.
import { from } from 'rxjs';
//create the "Hello world" observable
const myObs = from("Hello world");
Genial, myObs
está listo para emitir el flujo de datos «H,e,l,l,o, ,w,o,r,l,d». Este flujo contiene una letra por evento, incluido el espacio, obviamente.
Operadores
Pongamos ahora que no quiero espacios en la salida del flujo.
No hay problema: puedo usar el operador filter
de RxJS para eliminar eventos que coincidan con un espacio.
import { from } from 'rxjs';
//create the "Hello world" observable
const myObs = from("Hello world");
//apply the filter operator
myObs.pipe(
filter(char => char != ' ')
)
El mecanismo para añadir operadores (pipe
) te lo explicaré en breve.
De momento, fíjate que al operador filter
le pasas una función que recibe como entrada el evento a filtrar. filter
solo deja pasar eventos cuando la función de filtrado devuelve true
.
Vale, con esto deberías tener ya la secuencia «H,e,l,l,o,w,o,r,l,d», sin espacios.
Pero hay un problema. En realidad no se está ejecutando nada.
Suscripciones
Un Observable no se ejecuta mientras no tenga un Observer suscrito.
Grábate esto a fuego. Siempre.
Si quiero que se emitan los datos y se ejecute el operador de filtrado, necesito una Suscripción de un Observer.
import { from } from 'rxjs';
import { filter } from 'rxjs/operators';
//create the "Hello world" observable
const myObs = from("Hello world");
//apply the filter operator
const filteredObs = myObs.pipe(
filter(char => char != ' ')
);
//subscribe to the filtered observable
const subscription = filteredObs.subscribe(char => console.log(char));
Ahora si, mi salida por consola es la secuencia «H,e,l,l,o,w,o,r,l,d».
Las Suscripciones sirven, también, para cancelar el flujo de ejecución. Si yo quisiera interrumpir el flujo tras la letra «e», podría usar la suscripción así:
const subscription = filteredObs.subscribe(char => {
console.log(char);
if(char == 'e')
subscription.unsubscribe();
});
Y de este modo, solo se emitiría la secuencia «H,e».
RxJS Nivel PRO
Observers
Por cierto, la función char => console.log(char)
de la suscripción, es justamente el Observer (o mejor dicho, su función next
). Recuerda que te he definido el Observer como un objeto que recibe el flujo de datos y puede usar los valores recibidos. En este caso, simplemente los muestro por consola.
En realidad, un Observer puede ser más complejo. Su interfaz define 3 métodos:
next
: que es el que recibe y usa los datoserror
: para capturar los errores que pueda emitir un Observablecomplete
: para cerrar la suscripción (y por tanto el flujo de datos)
Podría escribir el código anterior con un Observer completo, así:
import { from } from 'rxjs';
import { filter } from 'rxjs/operators';
//create the "Hello world" observable
const myObs = from("Hello world");
//apply the filter operator
const filteredObs = myObs.pipe(
filter(char => char != ' ')
);
const observer = {
next: (evt) => console.log(evt),
error: (err) => console.error(err),
complete: () => console.log("completed")
}
//subscribe to the filtered observable
const subscription = filteredObs.subscribe(observer);
Encadenado de operadores (la función pipe
)
RxJS va de manipular flujos de datos. Y para eso tiene los operadores, como has visto.
Los operadores son funciones puras con una aproximación funcional:
- No modifican el objeto de entrada, sino que devuelven un objeto nuevo
- El resultado solo depende de la entrada y de la propia función
- Y se pueden encadenar
Esto no debería sonarte extraño. El mismo objeto Array
de Javascript tiene métodos que son funciones puras. Aquí tienes un ejemplo:
let array = new Array(1,2,3,4,5);
array.filter(x => x>3).map(x => x*2)
//output: [8,10]
En versiones anteriores a RxJS 5.5, los operadores de RxJS se aplicaban del mismo modo. Pero la forma de importarlos por separado era muy incómoda. Para solucionarlo, añadieron el método pipe
al objeto Observable.
El método pipe
te permite aplicar varios operadores sobre el flujo de datos de forma secuencial.
Por seguir con el ejemplo del «Hello world», imagina que además, quiero tener todas las letras en mayúsculas. Puedo conseguirlo con el operador map
de RxJS.
import { from } from 'rxjs';
import { filter, map } from 'rxjs/operators';
//create the "Hello world" observable
const myObs = from("Hello world");
const filteredObs = myObs.pipe(
filter(char => char != ' '),
map(char => char.toUpperCase())
);
//subscribe to the filtered observable
const subscription = filteredObs.subscribe(char => console.log(char));
//output: H E L L O W O R L D
Como ves, pipe
recibe un array de operadores, de modo que cada operador va modificando el flujo de datos. En este ejemplo, el operador map
no recibiría nunca un evento con el carácter de espacio (» «), porque el operador filter
ya se habría encargado de eliminar ese tipo de datos del flujo.
Ojo, no debes confundir el operador
map
conArray.map
. Son ideas similares, pero el operadormap
trabaja sobre un flujo de eventos, mientras queArray.map
trabaja sobre los elementos de un array.
Es importante que entiendas que en este ejemplo, la salida son 10 eventos separados. Uno con la letra «H», el siguiente con la «E», etc. En este caso son eventos consecutivos, pero podrían estar más espaciados en el tiempo.
¿Necesito una librería para gestionar flujos de datos?
Dímelo tu cuando acabe este ejemplo. Para que veas una pincelada de lo que es capaz RxJS, voy a modificar el código de modo que cada letra tenga un retraso de 1s con la anterior.
Te adelanto los nuevos elementos que voy a introducir:
- función
of
: Devuelve un Observable que emite un valor concreto - operador
delay
: Añade un retraso al inicio del flujo de datos - operador
concatMap
: Genera un nuevo observable a partir del evento recibido, se suscribe y emite sus valores hasta que termine. Luego, repite la operación con el siguiente evento que reciba.
import { from, of} from 'rxjs';
import { filter, map, delay, concatMap } from 'rxjs/operators';
const myObs = from("Hello world");
const filteredObs = myObs.pipe(
//here comes the magic 🙂
concatMap(char => of(char).pipe(delay(1000))),
filter(char => char != ' '),
map(char => char.toUpperCase()),
);
const subscription = filteredObs.subscribe(char => console.log(char));
Fíjate lo que consigo con solo una línea: Al recibir un evento (una letra en este caso), creo un nuevo Observable que emite únicamente esa letra, pero con un retraso de 1s. concatMap
se suscribe (para que se ejecute) y cuando termina (al cabo de 1s), repite la operación con el siguiente evento (la siguiente letra).
Es decir, con una sola línea he conseguido que ahora las letras se emitan con un espacio de 1s entre ellas.
A continuación puedes ver una animación que muestra como cada elemento de la pipe
manipula el flujo de datos en este ejemplo:
Resumen
Te he mostrado un ejemplo muy simple, que debería ayudarte a hacerte una imagen global de lo que es RxJS ¡Pero esto es solo la punta del Iceberg! En el próximo artículo te enseñaré algunos de los operadores más útiles que tiene RxJS.
Si no puedes esperar a saber más, te recomiendo que le des un vistazo a mi curso RxJS Nivel PRO, con un espectacular rating 4.9 sobre 5!!
Y como siempre, si te ha gustado este artículo… No te cortes, déjame un comentario y ayúdame a compartirlo 😉
RxJS Nivel PRO
const subscription = filteredObs.subscribe(char => {
console.log(char);
if(char == ‘e’)
subscription.unsubscribe();
});
Esta parte me da error
Error: Cannot read property ‘unsubscribe’ of undefined
Da ese error, pero igual cancela la subscripción.
Muy didactico gracias por la gran explicacion
Muy bien explicado bro!
Muy buena seria de artículos los que se han escrito para RxJS, se agradece la manera limpia, sencilla y clara de enseñanza.
Hola Enrique ante todo darte las gracias porque llevo siguiéndote desde hace tiempo y eres uno de los que ofrece explicaciones más claras y mejores en castellano.
Solo me queda una duda con este artículo y es saber porque si el método pipe es para concatenar operadores, porque cuando usamos solo el operador map también suele usarse.
Muchas gracias y un saludo