Programación Funcional, Asíncrona y Reactiva en JavaScript

Contents

Programación Funcional, Asíncrona y Reactiva en JavaScript#

La programación funcional es un paradigma de programación que ha ganado popularidad en los últimos años, especialmente con la adopción de JavaScript en la industria. Este enfoque de programación se centra en la evaluación declarativa, el uso de funciones puras y la inmutabilidad, entre otros principios. JavaScript, aunque es un lenguaje multiparadigma, ha incorporado muchas características que permiten programar de manera funcional.

La programación funcional se distingue por las siguientes características:

  • Evaluación declarativa vs. imperativa: En lugar de describir cómo hacer algo paso a paso (imperativa), se describe qué hacer (declarativa).

  • Funciones puras: Funciones que no tienen efectos secundarios y siempre producen el mismo resultado dado el mismo conjunto de argumentos.

  • Inmutabilidad: Evitar que una parte del programa modifique datos de forma inconsistente.

  • Sin efectos secundarios (side effects): Las funciones no deben alterar el estado del programa.

Es un paradigma, una manera de pensar y escribir código que busca reducir la programación imperativa y la programación orientada a objetos (POO).

No es obligatorio seguir absolutamente todos los principios de la programación funcional para programar bien en Javascript. Pero es interesante conocer los principios básicos para mejorar la calidad de código. La immutabilidad y las funciones puras siempre que se puedan aplicar son beneficiosas y permiten hacer programas más fáciles de mantener. No obstante, obsesionarse por la pureza de la programación funcional puede retrasar la programación, utilizar más recursos del ordenador o, incluso, hacer el código más difícil de leer.

Inmutabilidad#

La inmutabilidad es un principio clave en la programación funcional que implica que los datos no pueden ser modificados una vez creados. Esto ayuda a evitar errores y a mantener el código más predecible. La inmutabilidad total es imposible en cualquier programa que queremos que haga algo con los datos, pero debe ser controlada y acotada. Sobretodo nos preocuparemos de la mutabilidad del Estado de la aplicación.

Ejemplo de Mutación#

(()=>{
let foo = [1, 2, 3];
let bar = foo;
bar.push(10000);

console.log(foo); 
// Salida: [1, 2, 3, 10000]
})();
[ 1, 2, 3, 10000 ]

En este ejemplo, modificar el array bar también modifica foo porque ambos referencian el mismo array en memoria.

Evitar Mutaciones#

Podemos usar métodos como slice para evitar mutaciones:

(()=>{
const xs = [1, 2, 3, 4, 5];
// pura
const newArr = xs.slice(0, 3); 
console.log(newArr); 
// Salida: [1, 2, 3]
})();
[ 1, 2, 3 ]

También se puede usar el spread operator ([…xs]).

(()=>{
    const xs = [1, 2, 3, 4, 5];
    const newArr = [...xs]; 
    console.log(newArr); 
    })();
[ 1, 2, 3, 4, 5 ]

Para evitar la mutabilidad de las variables se puede:

  • Usar const (sólo funciona con variables primitivas, ya que los arrays son mutables internamente)

  • Reducir o eliminar las asignaciones

  • Usar estructuras de datos Inmutables como tuplas o records

  • Hacer copias de los objetos antes de modificarlos (… o structuredClone)

  • No asignar objetos por referencia.

  • Función freeze o seal

  • No usar funciones que provocan mutaciones en arrays, como splice, sort, push… como vimos en el capítulo de los arrays.

Purificación de Funciones#

En la programación funcional, todas las funciones deben cumplir ciertos requisitos:

  • Pocos argumentos: Idealmente, una función debe tener pocos argumentos.

  • Una sola tarea: Deben hacer solo una cosa.

  • Puras y deterministas: Deben ser funciones puras, es decir, no deben tener efectos secundarios y deben producir el mismo resultado para los mismos argumentos.

  • KISS

  • DRY

  • Trabajan sobre datos inmutables

  • Expresan sus dependencias

  • Son testables

  • Transparencia referencial: Una función siempre producirá el mismo resultado dado los mismos argumentos, sin causar efectos secundarios, lo que permite que cualquier expresión con la función pueda ser reemplazada por su valor de resultado sin alterar el comportamiento del programa. Sustituir la llamada a una función por su resultado almacenado en “caché” se llama meonizar.

Además de estas condiciones, también es aconsejable seguir estas consideraciones:

  • Si la función acepta un número indeterminado de argumentos es mejor aceptar un objeto o un array.

    • También se puede aceptar un número indeterminado y poner ...args para convertirlos en un array dentro de la función.

    • Más avanzado puede ser hacer Parameter Destructuring getify/Functional-Light-JS

  • Evitar que una función retorne más de un valor. Esto rompe el flujo y las hace más difíciles de leer.

  • Usar closures también se considera efecto colateral.

Funciones Impuras#

// Impura: modifica el entorno
console.log("Hola");

// Impura: no es determinista
Math.random();

// Impura: modifica el array como efecto colateral
array.splice(2, 3);

Funciones Puras#

// Función pura
const add = (a, b) => a + b;

// Función impura
let minimum = 21;
const checkAge = age => age >= minimum;

// Convertida a pura
const checkAgePure = (age, minimum = 21) => age >= minimum;

Las funciones impuras se detectan si:

  • No tiene argumentos de entrada

  • No retornan nada

  • Usa this

  • Usa variables globales

  • Modifica objetos que deberían ser inmutables.

  • El resultado no es determinista.

Lo más difícil de detectar es cuando aceptan un objeto y retorna un objeto que es el mismo mutado. Si no retornan una copia profunda del objeto, no son puras.

No todas las funciones pueden ser puras, la cuestión es delimitarlas para que no hagan daño.

Formas de Declarar Funciones#

Existen diversas maneras de declarar funciones en JavaScript, cada una con sus particularidades:

  • Declaración de función con nombre:

    function suma(a, b) {
      return a + b;
    }
    
  • Declaración de función anónima:

    const suma = function(a, b) {
      return a + b;
    };
    
  • Función flecha:

    const suma = (a, b) => a + b;
    
  • Expresión de función con function():

    const suma = function(a, b) {
      return a + b;
    };
    
Condiciones positivas para la programación funcional Declaración de función con nombre Declaración de función anónima Función flecha Expresión de función flecha Expresión de función con function()
En caso de error, podemos encontrar la función que ha fallado ✔ No siempre tiene el mismo nombre ✔ No siempre tiene el mismo nombre
Puede autoreferenciarse
La sintaxis es más consistente para argumentos y returns avanzados
Tiene hoisting
El código es breve

Funciones de Orden Superior#

Las funciones de orden superior son aquellas que aceptan o retornan otras funciones. En JavaScript, métodos como map, filter y reduce son funciones de orden superior. Estas son las más importantes y forman parte de los fundamentos de la programación funcional. Estas funciones forman parte del objeto Array.

En el capítulo de Iteradores tratamos su sintaxis con más detalle.

Podemos hacer nuestras propias funciones de órden superior.

Otras funciones de los objetos Array, algunas no totalmente funcionales, también son de órden superior y muy útiles: forEach, some, every, find, findIndex, flatMap, reduceRight

Ejemplo de map, filter y reduce#

(()=>{
    const numeros = [1, 2, 3, 4];
    const dobles = numeros.map(num => num * 2);
    console.log(dobles);
    // Salida: [2, 4, 6, 8]
})();
(() => {
    const numeros = [1, 2, 3, 4];
    const pares = numeros.filter(num => num % 2 === 0);
    console.log(pares);
    // Salida: [2, 4]
})();
  (() => {
    const numeros = [1, 2, 3, 4];
    const suma = numeros.reduce((acc, num) => acc + num, 0);
    console.log(suma);
    // Salida: 10
})();
  
[ 2, 4, 6, 8 ]
[ 2, 4 ]
10

Métodos Encadenados#

Los métodos encadenados permiten ejecutar múltiples métodos en una secuencia en una sola línea de código. Aunque no es 100% funcional (debido a los métodos prototípicos que pueden mutar el objeto original), los métodos encadenados facilitan la lectura y escritura del código. Estos métodos funcionan porque retornan un objeto del mismo tipo que el original o al menos con otros métodos encadenables.

No se aconseja añadir métodos encadenables a los prototipos de Array o similares, ya no por los principios de la programación funcional, sino por los problemas que pueden derivar de ellos.

let result = 'Hello World'.substring(0, 5).toLowerCase();
console.log(result);
// Salida: "hello"
hello

Currying#

Currificar consiste en transformar una función de múltiples argumentos en una secuencia de funciones que toman un único argumento (funciones unarias). Esto permite reutilizar mejor el código y mantener funciones puras.

Bibliotecas como Lodash o Ramda incorporan la posibilidad de currificar otras funciones existentes. Las funciones currificadas de esta manera pueden ser llamadas tanto con varios argumentos como por separado.

En realidad, el curring se basa en las closures, ya que se trata de funciones que aceptan un argumento y retornan otra función para aceptar el siguiente argumento.

Ejemplo de Currying#

(()=>{
const suma = (a, b) => a + b;

const sumaCurried = a => b => a + b;

console.log(sumaCurried(2)(3));
// Salida: 5
})();
5
(()=>{
const players = [{position: {x: 10, y: 20}},{position: {x: 15, y: 10}}];
const flag = {x: 0, y: 0}

const distance = (start, end) => Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2));

const distancewithCurrying = (start) => (end) => Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2));

const distances = players.map(player => distance(flag, player.position));
console.log("Distancias tradicionales: ", distances);

const distanceFromFlag = distancewithCurrying(flag);
const curriedDistances = players.map(player => player.position).map(distanceFromFlag)
console.log("Dist currificadas:", curriedDistances);
})();
Distancias tradicionales:  [ 22.360679774997898, 18.027756377319946 ]
Dist currificadas: [ 22.360679774997898, 18.027756377319946 ]

En el ejemplo anterior, supongamos un juego de capturar la bandera en el que esta siempre se queda en la misma posición. Para calcular la distancia no es necesario pasar siempre la posición. Otra manera de usar esta función puede ser esta:

const curriedDistances = players.map(player => player.position).map(distancewithCurrying(flag));

Así no es necesario crear la función distanceFromFlag;

El curring puede parecer innecesario, pero tiene ventajas, sobretodo con la siguiente técnica que es la composición. El hecho de poder tener siempre disponible funciones unarias simplifica en muchas ocasiones la programación.

Composición de Funciones#

La composición de funciones permite encadenar funciones de manera que el resultado de una función se pasa como entrada a la siguiente. Esta técnica es similar a lo que sucede al concatenar comandos en una tubería (|) en Linux. La composición permite construir operaciones complejas a partir de funciones más simples.

Aunque librerías como Ramda.js proporcionan funciones como compose() o pipe() para facilitar la composición, también podemos crear nuestras propias funciones de composición en JavaScript Vanilla.

Composición vs Pipelining#

La principal diferencia entre composición (compose) y pipelining (pipe) radica en el orden de ejecución de las funciones:

  • Compose: Ejecuta las funciones de derecha a izquierda.

  • Pipe: Ejecuta las funciones de izquierda a derecha.

Función compose#

La función compose toma un número variable de funciones como argumentos y devuelve una nueva función. Esta nueva función toma argumentos iniciales, ejecuta la última función con estos argumentos y luego pasa el resultado sucesivamente a través de las demás funciones en orden inverso.

const compose = 
  (...fns) => 
    (...args) => 
      fns.slice(0, -1).reverse().reduce(
        (memo, fn) => fn(memo),
        fns[fns.length - 1](...args)
      );

Aunque, con reduceRight el código queda más compacto:

compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

Esta función es igual que la anterior. Merece la pena dedicar un momento a leerla e intentar entender su funcionamiento.

Función pipe#

La función pipe es similar a compose, pero ejecuta las funciones en el orden en que se pasan, de izquierda a derecha.

const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

Aquí vemos un ejemplo simple del uso de la composición:

(()=>{
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

const toUpperCase = str => str.toUpperCase();
const exclaim = str => str + '!';

const shout = compose(exclaim, toUpperCase);

console.log(shout("hello"));
// Salida: "HELLO!"
})();
HELLO!

Otro ejemplo más avanzado consiste en crear una función de composición asíncrona para poder usar await en otras funciones asíncronas o que retornan promesas. Con esta se pueden componer funciones de, por ejemplo, pedir datos al servidor:

(()=>{
    const compose = (...fns) => x => fns.reduceRight(async(v, f) => f(await v), x);
    const addImgUrL = (artWork) => (artWork.img_url = `https://www.artic.edu/iiif/2/${artWork.image_id}/full/843,/0/default.jpg`,artWork);
    const addImgUrLArray = (artWorks) => artWorks.map(addImgUrL);
    const json = (request) => request.json();
    const getURL = async (url) =>  compose(json,fetch)(url); 
    const getArtWorks = async (url) => compose(addImgUrLArray,r=>r.data,getURL)(url); 
    const consoleLog = (datos) => console.log(datos.map(d=> ([d.img_url, d.title])));
    compose(consoleLog,getArtWorks)('https://api.artic.edu/api/v1/artworks');
})();

En este ejemplo se usa la composición varias veces y funciones puras muy breves que descomponen la tarea. Como se ve, si las funciones son unarias y retornan algo, pueden ser compuestas. La función de fetch no es pura porque conecta con el exterior y la función consoleLog tampoco porque no retorna nada y provoca un efecto colateral, pero las tenemos controladas. El pasar funciones como referencia a otras es llamado por algunos como point free style. En este ejemplo se concatenan muchas funciones dentro de compose sin indicar los argumentos con los que serán invocadas.

Algunas funciones importantes no son unarias y puede ser una buena idea convertirlas en funciones currificadas para usarlas en la composición. Por ejemplo, si queremos aplicar un map a un array en una función de composición, lo podemos hacer así:

compose(
    arr => arr.map(e => `<li>${e}</li>`),
    arr => arr.map(e => e*2)
)
(array);

Pero si creamos una función currificada como esta:

const MAP = (callback) => (array) => array.map(callback) 

La podemos escribir más elegantemente asi:

compose(
    MAP(e => `<li>${e}</li>`),
    MAP(e => e*2)
)
(array);

Pipeline Operator#

https://www.geeksforgeeks.org/javascript-pipeline-operator/

Puesto que esta funcionalidad ha sido añadida en 2024, puede que no esté disponible en todos los navegadores. Pero Babel lo soporta y lo puede transpilar.

function add(x) {
    return x + 10;
}
function subtract(x) {
    return x - 5;
}
// Without pipeline operator
let val1 = add(subtract(add(subtract(10))));
console.log(val1);

// Using pipeline operator

// First 10 is passed as argument to subtract
// function then returned value is passed to
// add function then value we get is passed to
// subtract and then the value we get is again
// passed to add function
let val2 = 10 |> subtract |> add |> subtract |> add;
console.log(val2);

Currificar y componer pueden parecer técnicas innecesarias y complicadas. Pero si las usamos bien y de forma que el código se entienda, puede quedar un código auto-documentado, más fácil de testar, reutilizable y más fácil de mantener. Por otro lado, son técnicas usadas continuamente dentro de los frameworks más conocidos y hay que saber leer ese código.

Proyectos Grandes Funcionales#

En proyectos grandes, es difícil mantener todas las funciones puras y evitar la mutabilidad completamente. Si se requiere trabajar con el paradigma funcional, hay que pensar en cómo hacer funcionalmente:

  • Gestión de Peticiones a la API: Las peticiones son por naturaleza impuras, por lo que hay que acotarlas.

  • Gestión del Estado Global: Utilizar librerías como Redux que están diseñadas con principios funcionales.

  • Gestión de la Navegación: Aplicar patrones funcionales para manejar la navegación en aplicaciones SPA (Single Page Application).

  • Gestión del DOM: Toda función que manipula el DOM es impura y tienden a hacer maś de una cosa al mismo tiempo. Este apartado suele ser el más difícil de hacer con PF y, por tanto, el que requiere más atención a los principios.

  • Web Components: Puesto que están hechos con clases y métodos, no cumplen estrictamente con los paradigmas, pero internamente y con la forma de crearlas se puede cumplir.

Problemas de la Programación Funcional#

Aunque la programación funcional tiene muchas ventajas, también presenta algunos problemas:

  • Complejidad: Puede ser difícil de entender y aplicar correctamente, especialmente para desarrolladores que no están familiarizados con el paradigma.

  • Rendimiento: La inmutabilidad y la creación de nuevas estructuras de datos pueden ser menos eficientes en términos de memoria y tiempo de ejecución. Hay que tener en cuenta que cada copia de arrays y objetos que hagamos permanece en memoria y hacer la copia tiene un coste computacional. Podemos confiar cierta optimización a los motores modernos de Javascript, por lo que muchas veces es mejor programar cómodo y seguro que óptimo.

  • Compatibilidad: No todos los frameworks o librerías están diseñados con principios funcionales en mente.

  • Concurrencia: La programación funcional puede ser menos intuitiva para manejar concurrencia en comparación con otros paradigmas.

En cuanto al rendimiento, cada operación funcional requiere hacer una copia de los datos. Si se trata de buscar la máxima optimización, debemos usar programación imperativa.

Observa este ejemplo:

(()=>{
// setup:
const numbers = Array.from({ length: 100_000 }).map(() => Math.random())

// 1. functional
let start = Date.now();
const resultF =
  numbers
    .map(n => Math.round(n * 10))
    .filter(n => n % 2 === 0)
    .reduce((a, n) => a + n, 0);
let times = {functional: Date.now()-start};


// 2. imperative
start = Date.now();
let resultI = 0
for (let i = 0; i < numbers.length; i++) {
  let n = Math.round(numbers[i] * 10)
  if (n % 2 !== 0) continue
  resultI = resultI + n
}
times.imperative = Date.now()-start;
console.log(times);
})();
{ functional: 10, imperative: 6 }

Ejemplo extraído de: https://romgrk.com/posts/optimizing-javascript#3-avoid-arrayobject-methods

Programación Reactiva#

La programación reactiva se basa en la idea de tener una interfaz que responde a cambios en el estado. Por ejemplo, si cambia el estado de una variable, la interfaz se renderiza nuevamente para reflejar este cambio. Implementar esto en JavaScript puro puede ser complicado, pero no imposible. Frameworks y librerías como Redux y RxJS facilitan esta tarea.

Programación Asíncrona#

Cualquier llamada a una API del navegador, como setTimeout, fetch, o addEventListener, se ejecuta de manera asíncrona. Para manejar esta asincronía, se pueden utilizar tres técnicas principales:

  1. Callbacks: Una función se pasa como argumento a otra función y se ejecuta después de que la operación asíncrona se completa.

  2. Promesas: Objetos que representan la eventual finalización (o falla) de una operación asíncrona y su valor resultante.

  3. Async/Await: Sintaxis más reciente que permite escribir código asíncrono de manera más síncrona y legible.

  4. Observables: En este caso necesitamos la librería RxJs, pero la sintaxis suele ser mucho más legible para situaciones complejas.

Paradigma Reactivo#

Cuando programamos de manera tradicional, utilizamos paradigmas imperativos, estructurados y orientados a objetos. Con la programación reactiva, aprovechamos técnicas de programación asíncrona y funcional para cambiar nuestra manera de pensar. Se trata de programar con flujos de datos asíncronos.

Lecturas recomendadas:

En la programación reactiva, todo se convierte en asíncrono: variables, propiedades, estructuras de datos, entrada del usuario, etc. Utilizamos herramientas para crear, combinar y filtrar estos flujos de datos, conocidos como streams.

Ejemplos de Aplicaciones Web Reactivas:

  1. Cambiar la apariencia de la web en función del scroll.

  2. Implementar un mecanismo de drag&drop gestionando la secuencia de eventos del ratón.

  3. Crear una caja de búsqueda reactiva.

  4. Validar formularios de manera reactiva.

  5. Implementar un chat en tiempo real.

  6. Crear un juego multijugador.

Un stream es una sucesión de eventos ordenados en el tiempo. Estos eventos pueden devolver un valor, un error o indicar el cierre del stream. Los eventos se capturan mediante funciones que se suscriben al stream. Estas funciones se llaman observadores (Observers).

En RxJS, los streams se denominan Observables.

RxJS#

RxJS (Reactive Extensions for JavaScript) permite manejar la asincronía utilizando el patrón de diseño Observable. Con RxJS, se pueden crear Observables a partir de cualquier evento posible. Un Observable emite valores cuando algún Observador se suscribe. Cada suscripción es independiente y tiene su propio contexto de ejecución. RxJS ofrece muchas formas de crear, filtrar y consumir Observables.

Mecanismo Unificado#

RxJS unifica varios mecanismos asincrónos bajo un solo modelo:

  • Promesas

  • Callbacks

  • Eventos

  • DOM

  • Webworkers

  • Websockets

Ejemplo Básico#

import { fromEvent } from 'rxjs';

document.addEventListener('DOMContentLoaded', () => {
    const button = document.querySelector('#miBoton');
    const miObservable = fromEvent(button, 'click');
    const subscription = miObservable.subscribe(event => console.log(event));
});

En este ejemplo, utilizamos fromEvent de RxJS para crear un Observable a partir del evento click de un botón. Luego, nos suscribimos al Observable para escuchar los eventos de click y los registramos en la consola.

Observables#

Los Observables son como “promesas mejoradas” o “promesas con esteroides”. A diferencia de las promesas, un Observable puede no cerrarse nunca. Los Observables son “perezosos” (lazy), es decir, no emiten nada hasta que tienen un suscriptor. Esto evita problemas de bucles de actualización y permite suscribirse y cancelar suscripciones fácilmente.

const fetchStream = new Observable((observer) => {
    fetch(url + ".json", parametros)
        .then((response) => response.json())
        .then((data) => {
            observer.next(data);
            observer.complete();
        })
        .catch((err) => observer.error(err));
});

En este ejemplo, creamos un Observable que realiza una solicitud fetch. Cuando la solicitud se completa, el Observable emite los datos obtenidos y luego se completa. Si ocurre un error, el Observable emite un error.

Observadores#

Un observador es un objeto que define cómo reaccionar a los valores emitidos por un Observable. Un observador puede definir tres funciones:

  • next: Se ejecuta cuando el Observable emite un valor.

  • error: Se ejecuta cuando el Observable emite un error.

  • complete: Se ejecuta cuando el Observable se completa.

const button = document.querySelector("button");
const observer = {
    next: function(value) { console.log(value); },
    error: function(err) { console.error(err); },
    complete: function() { console.log("Completed"); }
};

// Crear un Observable a partir de un evento
const observable = fromEvent(button, "click");

// Suscribirse para empezar a escuchar los resultados asíncronos
observable.subscribe(observer);

En este ejemplo, creamos un observador que define las funciones next, error y complete. Luego, nos suscribimos a un Observable creado a partir del evento click de un botón.

Múltiples Suscripciones#

Cada suscripción a un Observable es independiente y tiene su propio contexto de ejecución. Esto significa que para cada observador, el Observable puede emitir datos diferentes.

La desuscripción#

Es importante saber que la mayoría de suscripciones deben acabar. Algunas acaban porque el Obsevable emite un complete, pero otras no acaban fácilmente. Para acabar con una subscripción hay que llamar al método .unsubscribe() de los subscriptores.

Es muy común el error de abrir subscripciones y no cerrarlas, esto puede provocar multitud de subscriptores “fantasma” esperando al mismo observable. Si generamos Observables y subscriptores hay que recordar siempre pensar en cómo se acabará. Incluso aunque el motivo del Observable acabe, pueden quedar esas subscripciones residuales.

Si no es posible llamar a unsuscribe(), hay otras maneras de asegurarse que una subscripción acaba. Una manera puede ser usar el operador takeWhile o takeUntil que emiten un complete en ese Observable cuando se cumple una condición.

Una de las causas más comunes de problemas de rendimiento en aplicaciones reactivas son las subscripciones repetidas. Conforme los usuarios van accediendo de una ruta a otra, las subcripciones no desaparecen por culpa de las closures. Al final hay cientos de subscripciones, por ejemplo, a un mismo evento que repiten las mismas acciones.

Imaginemos un escenario en el que generamos un elemento del DOM que emite eventos y, al mismo tiempo, creamos un Observable y un suscriptor a ese evento. Si luego se elimina del DOM ese elemento, la subscripción no habrá desaparecido. Aunque nunca más se produzca el evento, el suscriptor está ocupando memoria. Con observables más activos como un interval, este problema es aún más grave, ya que se solaparán.

En Angular, los componentes tienen un ciclo de vida y en el momento de la destrucción del componente se ejecutan funciones que eliminan las subscripciones a los observables que forman la reactividad. Aún así, es importante implementar OnDestroy para desuscribir las subscripciones que no acaban por si mismas.

Desuscripciones en componentes en una SPA#

Si vamos a hacer una aplicación reactiva con un paradigma relativamente funcional, tendremos funciones que retornarán elementos. Podemos decidir hacer una suscripción dentro de esa función para que el elemento se actualice cuando el Observable emite un nuevo valor. El problema es que esa suscripción permanece dentro de la closure. Para forzar una desuscripción cuando ese elemento se elimina o es sustituido por otro, podemos adoptar varias estrategias:

  1. Usar MutationObserver:

function createReactiveElement(observable) {
  const element = document.createElement('div');
  const subscription = observable.subscribe(value => {
    element.textContent = `Valor actual: ${value}`;
  });
  const observer = new MutationObserver(mutations => {
    mutations.forEach(mutation => {
      mutation.removedNodes.forEach(node => {
        if (node === element) {
          // Desuscribimos el observable cuando el elemento es eliminado
          subscription.unsubscribe();
          observer.disconnect(); // Detenemos el observer
        }
      });
    });
  });
  // Configuramos el observer para observar el padre del elemento
  observer.observe(document.body, { childList: true, subtree: true });
  return element;
}

En este caso el observer escucha cuando se elimina ese elemento y fuerza su desuscripción.

  1. Manualmente mediante una función:

function createReactiveElement(observable) {
  const element = document.createElement('div');
  const subscription = observable.subscribe(value => {
    element.textContent = `Valor actual: ${value}`;
  });
  return {
    element,
    dispose() {
      subscription.unsubscribe(); // Desuscribimos el observable
    }
  };
}

const { element, dispose } = createReactiveElement(myObservable);
document.body.appendChild(element);

// Luego, eliminamos el elemento y llamamos a dispose para desuscribirnos
setTimeout(() => {
  document.body.removeChild(element);
  dispose(); // Cancelamos la suscripción
}, 5000);

Lo malo de esa opción és que hay que recordar ejecutar la función.

  1. Eventos personalizados

function createReactiveElement(observable) {
  const element = document.createElement('div');
  const subscription = observable.subscribe(value => {
    element.textContent = `Valor actual: ${value}`;
  });

  // Escuchar el evento personalizado para desuscribirse
  element.addEventListener('dispose', () => {
    subscription.unsubscribe();
  });

  return element;
}

// Crear y añadir el elemento reactivo al DOM
const reactiveElement = createReactiveElement(myObservable);
document.body.appendChild(reactiveElement);

// Enviar el evento 'dispose' antes de eliminar el elemento
setTimeout(() => {
  reactiveElement.dispatchEvent(new Event('dispose'));
  document.body.removeChild(reactiveElement);
}, 5000);

En este caso, aún siendo más flexible, hay que ejecutar dos funciones para eliminar un elemento.

  1. Web Components

Con los Web Components no hace falta saber cuando ni como se va a eliminar un elemento. Cambia mucho la forma de trabajar, pero es la más limpia:

class ReactiveElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' }); 
    this.shadowRoot.innerHTML = `<div>Valor actual: --</div>`;
    this.subscription = null; 
  }

  connectedCallback() {
    // Se llama cuando el componente es agregado al DOM
    this.subscription = myObservable.subscribe(value => {
      this.shadowRoot.querySelector('div').textContent = `Valor actual: ${value}`;
    });
  }

  disconnectedCallback() {
    // Se llama cuando el componente es removido del DOM
    if (this.subscription) {
      this.subscription.unsubscribe(); // Cancelamos la suscripción
    }
  }
}

Operadores#

Los operadores en RxJS son funciones puras que toman un Observable, manipulan sus emisiones y devuelven otro Observable. Estos operadores permiten transformar y combinar Observables de manera sencilla.

Pipe#

En RxJS (a partir de la versión 6), se pueden encadenar operadores usando pipe(), que utiliza la composición para concatenar funciones.

// Observable de valores de una caja de texto, encadenando operadores con pipe
inputValue
    .pipe(
        debounceTime(200), // espera una pausa de 200ms
        distinctUntilChanged(), // si el valor es el mismo, lo ignora
        // si llega un valor actualizado mientras la solicitud anterior sigue activa, cancela la solicitud anterior y 'cambia' al nuevo Observable
        switchMap(searchTerm => typeaheadApi.search(searchTerm))
    )
    .subscribe(results => {
        // actualizar el DOM
    });

En este ejemplo, utilizamos pipe para encadenar operadores. debounceTime espera una pausa de 200ms antes de emitir un valor, distinctUntilChanged ignora los valores que no cambian, y switchMap cancela la solicitud anterior si llega un nuevo valor. Finalmente, nos suscribimos al Observable para actualizar el DOM con los resultados.

Operadores Comunes#

RxJS proporciona una variedad de operadores que nos permiten manipular y transformar los datos emitidos por los Observables. Algunos de los operadores más comunes son:

map#

Transforma cada valor emitido por el Observable mediante una función dada.

import { from } from 'rxjs';
import { map } from 'rxjs/operators';

const numbers = from([1, 2, 3, 4, 5]);
const squaredNumbers = numbers.pipe(map(x => x * x));

squaredNumbers.subscribe(x => console.log(x)); // 1, 4, 9, 16, 25

filter#

Filtra los valores emitidos por el Observable según una condición dada.

import { from } from 'rxjs';
import { filter } from 'rxjs/operators';

const numbers = from([1, 2, 3, 4, 5]);
const evenNumbers = numbers.pipe(filter(x => x % 2 === 0));

evenNumbers.subscribe(x => console.log(x)); // 2, 4

tap#

Permite realizar efectos secundarios con los valores emitidos por el Observable sin modificar esos valores.

import { from } from 'rxjs';
import { tap } from 'rxjs/operators';

const numbers = from([1, 2, 3, 4, 5]);
const numbersWithSideEffect = numbers.pipe(tap(x => console.log('Side effect:', x)));

numbersWithSideEffect.subscribe(x => console.log('Received:', x));

first#

Emite solo el primer valor emitido por el Observable.

import { from } from 'rxjs';
import { first } from 'rxjs/operators';

const numbers = from([1, 2, 3, 4, 5]);
const firstNumber = numbers.pipe(first());

firstNumber.subscribe(x => console.log(x)); // 1

take#

Emite solo los primeros N valores emitidos por el Observable.

import { from } from 'rxjs';
import { take } from 'rxjs/operators';

const numbers = from([1, 2, 3, 4, 5]);
const firstThreeNumbers = numbers.pipe(take(3));

firstThreeNumbers.subscribe(x => console.log(x)); // 1, 2, 3

takeWhile#

Emite valores mientras se cumple una condición dada.

import { from } from 'rxjs';
import { takeWhile } from 'rxjs/operators';

const numbers = from([1, 2, 3, 4, 5]);
const numbersWhileLessThanFour = numbers.pipe(takeWhile(x => x < 4));

numbersWhileLessThanFour.subscribe(x => console.log(x)); // 1, 2, 3

takeUntil#

Emite valores hasta que otro Observable emita un valor.

import { interval, timer } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

const source = interval(1000); // emite cada segundo
const stopSignal = timer(5000); // se emite después de 5 segundos

const limitedSource = source.pipe(takeUntil(stopSignal));

limitedSource.subscribe(x => console.log(x));

last#

Emite solo el último valor emitido por el Observable.

import { from } from 'rxjs';
import { last } from 'rxjs/operators';

const numbers = from([1, 2, 3, 4, 5]);
const lastNumber = numbers.pipe(last());

lastNumber.subscribe(x => console.log(x)); // 5

takeLast#

Emite los últimos N valores emitidos por el Observable.

import { from } from 'rxjs';
import { takeLast } from 'rxjs/operators';

const numbers = from([1, 2, 3, 4, 5]);
const lastTwoNumbers = numbers.pipe(takeLast(2));

lastTwoNumbers.subscribe(x => console.log(x)); // 4, 5

skip#

Omite los primeros N valores emitidos por el Observable.

import { from } from 'rxjs';
import { skip } from 'rxjs/operators';

const numbers = from([1, 2, 3, 4, 5]);
const skipFirstTwoNumbers = numbers.pipe(skip(2));

skipFirstTwoNumbers.subscribe(x => console.log(x)); // 3, 4, 5

reduce#

Aplica una función acumuladora a las emisiones del Observable y devuelve el valor acumulado final.

import { from } from 'rxjs';
import { reduce } from 'rxjs/operators';

const numbers = from([1, 2, 3, 4, 5]);
const sum = numbers.pipe(reduce((acc, x) => acc + x, 0));

sum.subscribe(x => console.log(x)); // 15

scan#

Similar a reduce, pero emite el valor acumulado en cada paso.

import { from } from 'rxjs';
import { scan } from 'rxjs/operators';

const numbers = from([1, 2, 3, 4, 5]);
const runningSum = numbers.pipe(scan((acc, x) => acc + x, 0));

runningSum.subscribe(x => console.log(x)); // 1, 3, 6, 10, 15

Operadores Útiles#

Además de los operadores comunes, RxJS ofrece una serie de operadores útiles para casos específicos:

startWith#

Emite un valor inicial antes de los valores emitidos por el Observable.

import { of } from 'rxjs';
import { startWith } from 'rxjs/operators';

const numbers = of(2, 3, 4);
const numbersWithStart = numbers.pipe(startWith(1));

numbersWithStart.subscribe(x => console.log(x)); // 1, 2, 3, 4

endWith#

Emite un valor final después de los valores emitidos por el Observable.

import { of } from 'rxjs';
import { endWith } from 'rxjs/operators';

const numbers = of(1, 2, 3);
const numbersWithEnd = numbers.pipe(endWith(4));

numbersWithEnd.subscribe(x => console.log(x)); // 1, 2, 3, 4

distinct#

Filtra valores duplicados en base a todos los valores emitidos anteriormente.

import { of } from 'rxjs';
import { distinct } from 'rxjs/operators';

const numbers = of(1, 2, 2, 3, 4, 4, 5);
const distinctNumbers = numbers.pipe(distinct());

distinctNumbers.subscribe(x => console.log(x)); // 1, 2, 3, 4, 5

distinctUntilChanged#

Filtra valores duplicados consecutivos.

import { of } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';

const numbers = of(1, 2, 2, 3, 3, 4, 5, 5);
const distinctUntilChangedNumbers = numbers.pipe(distinctUntilChanged());

distinctUntilChangedNumbers.subscribe(x => console.log(x)); // 1, 2, 3, 4, 5

pairwise#

Emite el último valor emitido y el anterior como un array.

import { from } from 'rxjs';
import { pairwise } from 'rxjs/operators';

const numbers = from([1, 2, 3, 4, 5]);
const pairwiseNumbers = numbers.pipe(pairwise());

pairwiseNumbers.subscribe(x => console.log(x)); // [1, 2], [2, 3], [3, 4], [4, 5]

Operador share#

Si queremos tener dos suscripciones sin crear dos Observables, podemos compartir la suscripción con share(). Los Observables son “Cold” por defecto, lo que significa que cada suscripción tiene su propio flujo de datos. Share convierte un Cold Observable en un Hot Observable, permitiendo compartir la suscripción. Esto también se puede hacer con Subjects.

import { interval } from 'rxjs';
import { take, share } from 'rxjs/operators';

const source = interval(1000).pipe(take(5), share());

source.subscribe(x => console.log('Subscriber 1:', x));
setTimeout(() => {
    source.subscribe(x => console.log('Subscriber 2:', x));
}, 2000);

Operadores Temporales#

Los operadores temporales son útiles para manejar eventos en función del tiempo:

sampleTime#

Emite el último valor emitido por el Observable en intervalos de tiempo regulares.

import { interval } from 'rxjs';
import { sampleTime } from 'rxjs/operators';

const source = interval(1000); // emite cada segundo
const sampled = source.pipe(sampleTime(3000));

sampled.subscribe(x => console.log(x)); // 2, 5, 8, ...

throttleTime#

Emite un valor y luego ignora los valores subsiguientes durante un intervalo de tiempo especificado.

import { interval } from 'rxjs';
import { throttleTime } from 'rxjs/operators';

const source = interval(1000);
const throttled = source.pipe(throttleTime(3000));

throttled.subscribe(x => console.log(x)); // 0, 3, 6, 9, ...

auditTime#

Emite el último valor emitido por el Observable después de un intervalo de tiempo especificado, comenzando cuando llega un evento.

import { interval } from 'rxjs';
import { auditTime } from 'rxjs/operators';

const source = interval(1000);
const audited = source.pipe(auditTime(3000));

audited.subscribe(x => console.log(x)); // 2, 5, 8, ...

debounceTime#

Emite un valor después de que haya transcurrido un intervalo de tiempo sin que se hayan emitido nuevos valores.

import { fromEvent } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';

const searchBox = document.getElementById('searchBox');
const input$ = fromEvent(searchBox, 'input').pipe(
  map(event => event.target.value),
  debounceTime(300)
);

input$.subscribe(value => console.log(value));

delay#

Retrasa las emisiones del Observable por un intervalo de tiempo especificado.

import { of } from 'rxjs';
import { delay } from 'rxjs/operators';

const source = of('Hello');
const delayed = source.pipe(delay(2000));

delayed.subscribe(value => console.log(value)); // "Hello" después de 2 segundos

bufferTime#

Recoge valores emitidos por el Observable en un array y emite dicho array después de un intervalo de tiempo especificado.

import { interval } from 'rxjs';
import { bufferTime } from 'rxjs/operators';

const source = interval(500);
const buffered = source.pipe(bufferTime(2000));

buffered.subscribe(values => console.log(values)); // [0, 1, 2], [3, 4, 5], ...

Operadores de Espera#

Estos operadores son similares a los operadores temporales, pero esperan otro Observable en lugar de un intervalo de tiempo:

debounce#

Emite un valor solo después de que otro Observable haya emitido un valor y no se hayan emitido nuevos valores desde entonces.

import { fromEvent, interval } from 'rxjs';
import { debounce } from 'rxjs/operators';

const clicks = fromEvent(document, 'click');
const result = clicks.pipe(debounce(() => interval(1000)));

result.subscribe(x => console.log(x));

buffer#

Recoge valores emitidos por el Observable en un array y emite dicho array cuando otro Observable emite un valor.

import { fromEvent, interval } from 'rxjs';
import { buffer } from 'rxjs/operators';

const clicks = fromEvent(document, 'click');
const intervalEvents = interval(1000);
const buffered = intervalEvents.pipe(buffer(clicks));

buffered.subscribe(values => console.log(values));

sample#

Emite el último valor emitido por el Observable cuando otro Observable emite un valor.

import { fromEvent, interval } from 'rxjs';
import { sample } from 'rxjs/operators';

const clicks = fromEvent(document, 'click');
const intervalEvents = interval(1000);
const sampled = clicks.pipe(sample(intervalEvents));

sampled.subscribe(x => console.log(x));

throttle#

Emite un valor y luego ignora los valores subsiguientes durante un intervalo de tiempo especificado, hasta que otro Observable emita un valor.

import { fromEvent, interval } from 'rxjs';
import { throttle } from 'rxjs/operators';

const clicks = fromEvent(document, 'click');
const intervalEvents = interval(1000);
const throttled = clicks.pipe(throttle(() => intervalEvents));

throttled.subscribe(x => console.log(x));

audit#

Emite el último valor emitido por el Observable cuando otro Observable emite un valor, comenzando cuando llega un evento.

import { fromEvent, interval } from 'rxjs';
import { audit } from 'rxjs/operators';

const clicks = fromEvent(document, 'click');
const intervalEvents = interval(1000);
const audited = clicks.pipe(audit(() => intervalEvents));

audited.subscribe(x => console.log(x));

Ejemplo de Uso Combinado de Operadores#

Para ilustrar el uso combinado de operadores, consideremos un ejemplo práctico donde queremos implementar una búsqueda en tiempo real en una caja de texto, con debouncing para evitar demasiadas peticiones al servidor:

import { fromEvent } from 'rxjs';
import { map, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';

const searchBox = document.getElementById('searchBox');
const typeahead = fromEvent(searchBox, 'input').pipe(
  map(event => event.target.value),
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(searchTerm => fetch(`/api/search?q=${searchTerm}`).then(response => response.json()))
);

typeahead.subscribe(results => {
  // Actualizar el DOM con los resultados
  console.log(results);
});

En este ejemplo:

  • fromEvent crea un Observable de eventos de entrada en el campo de búsqueda.

  • map extrae el valor del campo de búsqueda.

  • debounceTime espera 300 ms después de la última entrada del usuario.

  • distinctUntilChanged evita emitir valores si el término de búsqueda no ha cambiado.

  • switchMap realiza una petición de búsqueda al servidor y emite los resultados.

Con esto, hemos cubierto una variedad de operadores en RxJS, mostrando cómo se pueden utilizar para gestionar y transformar flujos de datos de manera eficiente y reactiva. Estos operadores son fundamentales para implementar patrones de diseño reactivos y manejar la asincronía de manera clara y concisa en nuestras aplicaciones.

Combinación de Observables en RxJS#

zip()#

Combina dos Observables en un array cuando ambos tienen datos disponibles, emitiendo un array con las últimas emisiones de cada Observable.

import { of, zip } from 'rxjs';

const source1 = of('Hello');
const source2 = of('World');

const result = zip(source1, source2);
result.subscribe(val => console.log(val)); // ['Hello', 'World']

merge()#

Combina dos Observables en uno solo sin esperar a tener datos de ambos. Emite valores tan pronto como cada Observable los emite.

import { interval, merge } from 'rxjs';
import { map } from 'rxjs/operators';

const source1 = interval(1000).pipe(map(x => `Source 1: ${x}`));
const source2 = interval(1500).pipe(map(x => `Source 2: ${x}`));

const merged = merge(source1, source2);
merged.subscribe(val => console.log(val));

concat()#

Espera a que un Observable complete antes de emitir valores del siguiente Observable en la secuencia.

import { of, concat } from 'rxjs';

const source1 = of('Hello');
const source2 = of('World');

const result = concat(source1, source2);
result.subscribe(val => console.log(val)); // 'Hello', 'World'

forkJoin()#

Espera a que todos los Observables completen y luego emite un array con los últimos valores emitidos por cada Observable.

import { of, forkJoin } from 'rxjs';
import { delay } from 'rxjs/operators';

const source1 = of('Hello').pipe(delay(1000));
const source2 = of('World').pipe(delay(2000));

const result = forkJoin([source1, source2]);
result.subscribe(val => console.log(val)); // ['Hello', 'World']

combineLatest()#

Combina múltiples Observables y emite un array con los últimos valores de cada uno tan pronto como cualquiera de ellos emite un nuevo valor.

import { combineLatest, interval } from 'rxjs';
import { map } from 'rxjs/operators';

const source1 = interval(1000).pipe(map(x => `Source 1: ${x}`));
const source2 = interval(1500).pipe(map(x => `Source 2: ${x}`));

const combined = combineLatest([source1, source2]);
combined.subscribe(val => console.log(val));

withLatestFrom()#

Combina el valor más reciente de otro Observable cuando el primer Observable emite un valor.

import { interval } from 'rxjs';
import { withLatestFrom, map } from 'rxjs/operators';

const source1 = interval(1000);
const source2 = interval(1500);

const combined = source1.pipe(
  withLatestFrom(source2),
  map(([first, second]) => `First: ${first}, Second: ${second}`)
);

combined.subscribe(val => console.log(val));

High Order Observables (HOO)#

Los High Order Observables son aquellos que emiten otros Observables.

mergeAll()#

Convierte Observables internos en Observables externos, emitiendo todos los valores de manera concurrente.

import { of } from 'rxjs';
import { map, mergeAll } from 'rxjs/operators';

const source = of('Hello', 'World').pipe(
  map(val => of(val).pipe(delay(1000))),
  mergeAll()
);

source.subscribe(val => console.log(val));

mergeMap()#

Transforma y mapea los Observables internos, emitiendo todos los valores de manera concurrente.

import { fromEvent, of } from 'rxjs';
import { mergeMap, delay } from 'rxjs/operators';

const clicks = fromEvent(document, 'click');
const result = clicks.pipe(
  mergeMap(() => of('Hello').pipe(delay(1000)))
);

result.subscribe(val => console.log(val));

switchMap()#

Como mergeMap pero cancela el Observable interno anterior si llega un nuevo evento al Observable externo.

import { fromEvent, of } from 'rxjs';
import { switchMap, delay } from 'rxjs/operators';

const clicks = fromEvent(document, 'click');
const result = clicks.pipe(
  switchMap(() => of('Hello').pipe(delay(1000)))
);

result.subscribe(val => console.log(val));

concatMap()#

Como mergeMap pero mantiene el orden, esperando que cada Observable interno complete antes de emitir el siguiente.

import { of } from 'rxjs';
import { concatMap, delay } from 'rxjs/operators';

const source = of('Hello', 'World').pipe(
  concatMap(val => of(val).pipe(delay(1000)))
);

source.subscribe(val => console.log(val));

Arrays a Eventos#

Si un Observable emite un array y queremos un evento para cada elemento, podemos usar mergeMap con from dentro.

import { of, from } from 'rxjs';
import { mergeMap } from 'rxjs/operators';

of([1, 2, 3, 4, 5, 6, 7, 8])
  .pipe(
    mergeMap(arr => from(arr)),
    mergeMap(n => fakeFetch(n)) // Suponiendo que fakeFetch es una función que retorna un Observable
  )
  .subscribe(n => document.querySelector('#divMergeMap').innerHTML += n + ', ');

function fakeFetch(n) {
  return of(n).pipe(delay(500));
}

Subjects en RxJS#

Concepto de Subjects#

Los Subjects en RxJS permiten compartir una misma emisión entre varios suscriptores. A diferencia de los Observables tradicionales, que mantienen una ejecución independiente para cada suscriptor, los Subjects pueden compartir las emisiones con todos sus suscriptores. Cada Subject actúa tanto como un Observable como un Observer, lo que significa que puede emitir valores y también suscribirse a otros Observables.

Ejemplo básico de Subject:

import { Subject, from } from 'rxjs';

const subject = new Subject();

subject.subscribe({
  next: (v) => console.log(`observerA: ${v}`)
});
subject.subscribe({
  next: (v) => console.log(`observerB: ${v}`)
});

subject.next(1); // Both observers log: 1
subject.next(2); // Both observers log: 2

const observable = from([10, 20, 30]);
observable.subscribe(subject); 
// Observers log: 10, 20, 30

BehaviorSubject#

El BehaviorSubject mantiene un valor actual y emite este valor cada vez que un nuevo suscriptor se une. Necesita un valor inicial y permite a los suscriptores obtener el último valor emitido incluso antes de que se suscribieran.

Ejemplo de BehaviorSubject:

import { BehaviorSubject } from 'rxjs';

const bSubject = new BehaviorSubject(100);

bSubject.subscribe({
  next: (v) => console.log(`observerA: ${v}`)
});

bSubject.next(101); // observerA logs: 101
bSubject.next(102); // observerA logs: 102

bSubject.subscribe({
  next: (v) => console.log(`observerB: ${v}`)
});
// observerB logs: 102

bSubject.next(103); 
// Both observers log: 103

Operadores de errores#

throwError()#

Este operador crea un Observable que emite un error.

catchError()#

Captura errores emitidos por el Observable y permite manejarlos sin romper el flujo de ejecución.

retry()#

Reintenta la operación un número limitado de veces en caso de error.

Ejemplo de manejo de errores:

import { fromFetch } from 'rxjs/fetch';
import { switchMap, catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

async function getData(url) {
  return fromFetch(url).pipe(
    switchMap(response => {
      if (!response.ok) {
        return throwError(new Error(`Error de servidor: ${response.status} ${response.statusText}`));
      }
      return response.json();
    }),
    catchError(error => {
      console.error('Error:', error.message);
      throw error;
    })
  );
}

async function fetchExample() {
  const url = 'https://api.foo.com/data';

  getData(url).subscribe(
    data => {
      console.log('Datos recibidos:', data);
      // Hacer algo con los datos recibidos
    },
    error => {
      console.error('Error en la solicitud:', error);
      // Manejar el error de alguna manera
    }
  );
}

fetchExample();

Ejemplos prácticos de Observables#

Observable de un Click#

import { fromEvent } from 'rxjs';

const button = document.querySelector('#miBoton');
const miObservable = fromEvent(button, "click");

const subscription = miObservable.subscribe(event =>
  console.log(event, "1")
);

Observable para sumar un array#

Versión funcional:

const arrayNumeros = [1, 2, 3, 4, 5, 6, 7, "a", [11, 22, 33], { a: 5 }];
const resultado = arrayNumeros
  .map((n) => parseInt(n))
  .filter((n) => !isNaN(n));

const suma = resultado.reduce((x, y) => x + y);
console.log(resultado, suma);

Versión reactiva:

import { from } from 'rxjs';
import { map, filter, reduce } from 'rxjs/operators';

const arrayNumeros = [1, 2, 3, 4, 5, 6, 7, "a", [11, 22, 33], { a: 5 }];
const instantSum = from(arrayNumeros).pipe(
  map((n) => parseInt(n)),
  filter((n) => !isNaN(n)),
  reduce((x, y) => x + y)
).subscribe(n => console.log(n));

Observable en un Intervalo#

import { interval } from 'rxjs';
import { take, map } from 'rxjs/operators';

const arrayNumeros = [1, 2, 3, 4, 5, 6, 7, "a", [11, 22, 33], { a: 5 }];
const source = interval(500).pipe(
  take(10),
  map((i) => arrayNumeros[i])
);

source.subscribe(x => console.log(x));

Capturar un Doble Click#

import { fromEvent } from 'rxjs';
import { buffer, debounceTime, filter } from 'rxjs/operators';

const dClick = document.querySelector("#dobleClick");
const dClickObservable = fromEvent(dClick, "click");

const dClickBuffer = dClickObservable.pipe(
  buffer(dClickObservable.pipe(debounceTime(250)))
);

dClickBuffer.subscribe(n => console.log(n));
dClickBuffer.pipe(
  filter(clickArray => clickArray.length > 1)
).subscribe(() => console.log('Doble Click!!!'));

Observable para un Fetch#

Básico:

import { from } from 'rxjs';

const response = from(fetch(url).then(response => response.json()));

Mejorado con manejo de next y error:

import { Observable } from 'rxjs';

const fetchStream = new Observable(async (observer) => {
  try {
    const response = await fetch(url + ".json", parametres);
    const data = await response.json();
    observer.next(data);
    observer.complete();
  } catch (err) {
    observer.error(err);
  }
});

Estado de la aplicación#

Para controlar el estado de la aplicación, se necesita manejar diversas variables como respuestas del servidor, cache, datos locales, secciones activas y la página actual. Con la programación reactiva, el estado también se puede mantener de manera reactiva utilizando RxJS, aunque Redux es una librería más específica para la gestión del estado.

Redux#

Redux se basa en tres principios:

  1. Una única fuente de verdad: Todo el estado de la aplicación está almacenado en un único objeto de estado.

  2. El estado es de solo lectura: La única forma de cambiar el estado es emitiendo una acción, un objeto que describe lo que ocurrió.

  3. Los cambios son realizados mediante funciones puras: Para especificar cómo cambia el estado en respuesta a una acción, se escriben reducers.

En Redux, el estado se mantiene en una Store, que acepta un estado inicial y reducers. Podemos suscribirnos a los cambios en la Store y actualizar la interfaz de usuario en consecuencia.

Recursos útiles: