Reagent, una libería para usar ReactJS desde ClojureScript

Después de conocer un poco con ReactJS, hacer un pequeño ejemplo con ReactJS y ver cómo podíamos automatizar la compilación de aplicaciones basadas en ReactJS, ha llegado el momento de cambiar de lenguaje y ver cómo podemos utilizar ReactJS desde ClojureScript.

ClojureScript es una implementación de Clojure que permite compilar código Clojure a Javascript, con lo que podemos ejecutarlo en el browser o incluso en plataformas como Node.js, en lugar de usar la JVM como motor de ejecución. Como era de esperar, con ClojureScript podemos utilizar las APIs nativas del navegador, pero además existen librerías que nos permiten trabajar de una forma más idiomática.

En el caso concreto de ReactJS, hay varias alternativas para utilizarlo desde ClojureScript. La más completa seguramente sea Om, que no sólo ofrece un interface sobre ReactJS sino que además aprovecha otras características «modernas» de Clojure (como los canales de core.async). Esto hace que sea una librería algo más intrusiva que el propio ReactJS, puesto que cubre un área mayor de la aplicación en lugar de limitarse a la pura generación del DOM.

En este post vamos a ver otra alternativa, reagent, que se define a si misma como un interface minimalista entre ReactJS y ClojureScript, y que no es más que eso, una pequeña capa de abstracción sobre ReactJS para poder escribir componentes de ReactJS de una forma más cómoda desde ClojureScript.

¿Por qué ClojureScript?

Dada la cantidad de lenguajes que compilan (o transpilan, según se mire) a Javascript, es razonable preguntarse por qué usar ClojureScript en lugar de, por ejemplo, TypeScript o ClojureScript.

Dejando de lado las bondades de Clojure como lenguaje, hay un par de factores que hacen que me parezca especialmente interesante la integración entre ClojureScript y ReactJS.

Por una parte, como vimos en el primer post de esta serie, ReactJS hace una distinción muy férrea entre la información mutable (state) y la información inmutable (props). Esto cuadra muy bien con la filosofía de Clojure, en la que por defecto la información es inmutable y existen mecanismos, como los atom para gestionar de forma explícita la mutabilidad.

Esto hace además que podamos mejorar el ya de por si excelente rendimiento de ReactJS. ReactJS utiliza un modelo de DOM Virtual sobre el que calcula diferencias para optimizar el renderizado en pantalla reduciendo al máximo el número de operaciones lanzadas sobre el DOM real. Para ello, necesita comparar las distintas versiones del DOM virtual y así detectar los cambios. Puesto que la mayoría de la información en Clojure se almacena en estructuras de datos persistentes (inmutables), esta comparación puede hacerse muchas veces realizando una simple comparación por referencia, lo que evita tener que recorrer estructuras de datos complejas para detectar cambios, acelerando así el proceso de renderizado aún más.

Por otro lado, y esto es una cuestión más personal, en ReactJS se utiliza una sintaxis especial, el JSX, para «mezclar» javascript con HTML a la hora de generar el DOM. En Clojure es frecuente utilizar técnicas similares, con librerías como Hiccup, por lo que esta forma de trabajar resulta bastante idiomática.

Un ejemplo

No voy a entrar en mucho detalle de cómo funciona ClojureScript (eso daría para un par de posts), pero vamos a ver un ejemplo de uso muy básico de reagent para poder compararlo con ReactJS.

Partiremos del mismo ejemplo de la lista de cervezas, en el que pretendíamos construir un interface como éste:

react-sample

En ReactJS, la forma de implementar cada componente era definiendo un objeto a través de la función React.createClass:

var BeerList = React.createClass({
  render: function() {...}
});

Con reagent, la forma más simple de implementar una función es a través de una función. Un componente en reagent no es más que una función que devuelve el DOM que se debe generar. Para ello, cuenta con una sintaxis parecida a la de hiccup, y la información inmutable que recibe el componente (las props de ReactJS) son directamente los parámetros que recibe la función. Existen otras alternativas para cubrir casos más complejos, pero de momento nos quedaremos con esta forma de trabajar.

Partiendo de esto, el componente que muestra la información de una cerveza sería algo tan simple como esto:

(defn beer-item [beer count]
  [:li "[" count "] " beer
   [:button {:on-click #(add-one! beer)} "Otra más"]])

Se trata de una función que recibe el nombre de la cerveza (beer) y las unidades consumidas hasta el momento (count) y genera el DOM necesario.

Para el componente que genera la lista completa, tendríamos algo así:

(defn beer-list []
  (let [total (reduce + (vals @beers))
        items (map (fn [[name count]] (beer-item name count)) @beers)]
    [:div
     [:p "Llevas " total  " cervezas"]
     [:ul items]]))

Aquí podemos ver una diferencia entre la manera de gestionar el estado mutable en ReactJS y en Reagent. En ReactJS es necesario utilizar las funciones getInitialState, setState y getState para tratar con el estado mutable, porque era necesario que la librería pudiese detectar cambios en el estado y así forzar el renderizado del componente que poseía ese estado.

En reagent se utilizan atoms para almacenar el estado mutable. Estos atoms no son exactamente los mismos que en Clojure, sino que son una implementación propia de reagent con algunas características especiales, pero exponen el mismo API que un atom de Clojure (y que podéis consultar en este post). A través de los atoms reagent puede detectar cuando se producen cambios en el estado y volver a renderizar a los componentes necesarios.

Aunque en el ejemplo se está utilizando un atom definido globalmente, es posible trabajar con atoms locales a cada componente si tiene más sentido.

El código completo del ejemplo sería éste:

(ns reagent-cljs.core
  (:require [reagent.core :as r]))

(enable-console-print!)

(def beers
  (r/atom {"Mahou 5 Estrellas" 0
           "Chimay triple" 0
           "Cibeles Imperial IPA" 0}))

(defn add-one! [beer]
  (swap! beers update-in [beer] inc))

(defn beer-item [beer count]
  [:li "[" count "] " beer
   [:button {:on-click #(add-one! beer)} "Otra más"]])

(defn beer-list []
  (let [total (reduce + (vals @beers))
        items (map (fn [[name count]] (beer-item name count)) @beers)]
    [:div
     [:p "Llevas " total  " cervezas"]
     [:ul items]]))

(r/render-component [beer-list] (.-body js/document))

Si tenéis curiosidad por ver cómo se estructura un proyecto de ClojureScript (o al menos una de las formas de estructurarlo), podéis echarle un vistazo en GitHub.

Una vez instalado leiningen y clonado el repositorio, tan sólo necesitáis ejecutar:

lein cljsbuild auto

El código se generará cada vez que se modifique algún fichero y podréis ver la página en funcionamiento abriendo el archivo index.html de la raíz del proyecto.

Resumen

Algunas de las ideas subyacentes a ReactJS hacen que encaje muy bien con un lenguaje como ClojureScript, especialmente por el tratamiento de la (im)mutabilidad y, en menor medida, por el uso de una sintaxis embebida en el propio lenguaje para la generación del DOM.

Reagent ofrece un interface simple sobre ReactJS para hacer que usarlo desde ClojureScript sea una experiencia muy cómoda. Como se puede ver en el ejemplo, el código que resulta mantiene un estilo muy típico de Clojure y aprovecha bastante bien las características de este lenguaje.