Control de nivel superior con Redux State Management: un tutorial de ClojureScript

Publicado: 2022-03-11

¡Bienvenido de nuevo a la segunda entrega emocionante de Unearthing ClojureScript! En esta publicación, cubriré el próximo gran paso para tomarse en serio ClojureScript: administración de estado, en este caso, usando React.

Con el software front-end, la gestión del estado es un gran problema. Fuera de la caja, hay un par de formas de manejar el estado en React:

  • Mantener el estado en el nivel superior y pasarlo (o los controladores para un estado en particular) a los componentes secundarios.
  • Tirar la pureza por la ventana y tener variables globales o alguna forma Lovecraftiana de inyección de dependencia.

En términos generales, ninguno de estos es genial. Mantener el estado en el nivel superior es bastante simple, pero luego hay una gran cantidad de sobrecarga para pasar el estado de la aplicación a cada componente que lo necesita.

En comparación, tener variables globales (u otras versiones ingenuas de estado) puede generar problemas de concurrencia difíciles de rastrear, lo que hace que los componentes no se actualicen cuando se espera, o viceversa.

Entonces, ¿cómo se puede abordar esto? Para aquellos de ustedes que están familiarizados con React, es posible que hayan probado Redux, un contenedor de estado para aplicaciones de JavaScript. Es posible que haya encontrado esto por su propia voluntad, buscando audazmente un sistema manejable para mantener el estado. O es posible que se haya topado con él mientras leía sobre JavaScript y otras herramientas web.

Independientemente de cómo las personas terminen mirando a Redux, en mi experiencia, generalmente terminan con dos pensamientos:

  • “Siento que tengo que usar esto porque todos dicen que tengo que usarlo”.
  • “Realmente no entiendo completamente por qué esto es mejor”.

En términos generales, Redux proporciona una abstracción que permite que la gestión del estado encaje dentro de la naturaleza reactiva de React. Al descargar todo el estado a un sistema como Redux, conservas la pureza de React. Por lo tanto, terminará con muchos menos dolores de cabeza y, en general, algo sobre lo que es mucho más fácil razonar.

Para aquellos nuevos en Clojure

Si bien esto puede no ayudarlo a aprender ClojureScript completamente desde cero, aquí al menos recapitularé algunos conceptos básicos de estado en Clojure[Script]. ¡Siéntete libre de saltarte estas partes si ya eres un clojuriano experimentado!

Recuerde uno de los conceptos básicos de Clojure que también se aplica a ClojureScript: de forma predeterminada, los datos son inmutables. Esto es genial para desarrollar y tener garantías de que lo que creas en el paso de tiempo N sigue siendo el mismo en el paso de tiempo > N. ClojureScript también nos brinda una forma conveniente de tener un estado mutable si lo necesitamos, a través del concepto de atom .

Un atom en ClojureScript es muy similar a una AtomicReference en Java: proporciona un nuevo objeto que bloquea su contenido con garantías de concurrencia. Al igual que en Java, puedes colocar lo que quieras en este objeto; a partir de ese momento, ese átomo será una referencia atómica a lo que quieras.

Una vez que tenga su atom , puede establecer atómicamente un nuevo valor usando el reset! función (tenga en cuenta el ! en la función; en el lenguaje Clojure, esto se usa a menudo para indicar que una operación tiene estado o es impura).

También tenga en cuenta que, a diferencia de Java, a Clojure no le importa lo que pone en su atom . Podría ser una cadena, una lista o un objeto. Escritura dinámica, bebé!

 (def my-mutable-map (atom {})) ; recall that {} means an empty map in Clojure (println @my-mutable-map) ; You 'dereference' an atom using @ ; -> this prints {} (reset! my-mutable-map {:hello "there"}) ; atomically set the atom (reset! my-mutable-map "hello, there!") ; don't forget Clojure is dynamic :)

Reactivo amplía este concepto de átomo con su propio atom . (Si no está familiarizado con Reagent, consulte la publicación anterior). Esto se comporta de manera idéntica al atom de ClojureScript, excepto que también desencadena eventos de procesamiento en Reagent, al igual que el almacén de estado incorporado de React.

Un ejemplo:

 (ns example (:require [reagent.core :refer [atom]])) ; in this module, atom now refers ; to reagent's atom. (def my-atom (atom "world!")) (defn component [] [:div [:span "Hello, " @my-atom] [:input {:type "button" :value "Press Me!" :on-click #(reset! My-atom "there!")}]])

Esto mostrará un solo <div> que contiene un <span> que dice "¡Hola, mundo!" y un botón, como era de esperar. Presionar ese botón mutará atómicamente my-atom para contener "there!" . Eso activará un redibujado del componente, lo que dará como resultado que el lapso diga "¡Hola, ahí!" en lugar de.

Descripción general de la gestión del estado como lo manejan Redux y Reagent.

Esto parece lo suficientemente simple para la mutación local a nivel de componente, pero ¿qué pasa si tenemos una aplicación más complicada que tiene múltiples niveles de abstracción? ¿O si necesitamos compartir un estado común entre varios subcomponentes y sus subcomponentes?

Un ejemplo más complicado

Exploremos esto con un ejemplo. Aquí implementaremos una página de inicio de sesión cruda:

 (ns unearthing-clojurescript.login (:require [reagent.core :as reagent :refer [atom]])) ;; -- STATE -- (def username (atom nil)) (def password (atom nil)) ;; -- VIEW -- (defn component [on-login] [:div [:b "Username"] [:input {:type "text" :value @username :on-change #(reset! username (-> % .-target .-value))}] [:b "Password"] [:input {:type "password" :value @password :on-change #(reset! password (-> % .-target .-value))}] [:input {:type "button" :value "Login!" :on-click #(on-login @username @password)}]])

Luego alojaremos este componente de inicio de sesión dentro de nuestro app.cljs principal, así:

 (ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- STATE (def token (atom nil)) ;; -- LOGIC -- (defn- do-login-io [username password] (let [t (complicated-io-login-operation username password)] (reset! token t))) ;; -- VIEW -- (defn component [] [:div [login/component do-login-io]])

El flujo de trabajo esperado es así:

  1. Esperamos a que el usuario ingrese su nombre de usuario y contraseña y presione enviar.
  2. Esto activará nuestra función do-login-io en el componente principal.
  3. La función do-login-io realiza algunas operaciones de E/S (como iniciar sesión en un servidor y recuperar un token).

Si esta operación está bloqueando, entonces ya estamos en un montón de problemas, ya que nuestra aplicación está congelada; si no es así, ¡entonces tenemos que preocuparnos por async!

Además, ahora debemos proporcionar este token a todos nuestros subcomponentes que quieran realizar consultas a nuestro servidor. ¡La refactorización de código ahora es mucho más difícil!

Finalmente, nuestro componente ya no es puramente reactivo : ahora es cómplice en la gestión del estado del resto de la aplicación, desencadenando E/S y, en general, siendo un poco molesto.

Tutorial de ClojureScript: Ingrese a Redux

Redux es la varita mágica que hace realidad todos tus sueños basados ​​en el estado. Si se implementa correctamente, proporciona una abstracción de estado compartido que es segura, rápida y fácil de usar.

El funcionamiento interno de Redux (y la teoría detrás de él) están un poco fuera del alcance de este artículo. En su lugar, me sumergiré en un ejemplo de trabajo con ClojureScript, que con suerte debería demostrar de alguna manera de lo que es capaz.

En nuestro contexto, Redux está implementado por una de las muchas bibliotecas de ClojureScript disponibles; este llamado re-encuadre. Proporciona un envoltorio Clojure-ified alrededor de Redux que (en mi opinión) hace que su uso sea un placer absoluto.

Los basicos

Redux eleva el estado de su aplicación, dejando sus componentes livianos. Un componente reducido solo necesita pensar en:

  • lo que parece
  • Que datos consume
  • Qué eventos desencadena

El resto se maneja entre bastidores.

Para enfatizar este punto, reduzcamos nuestra página de inicio de sesión anterior.

La base de datos

Lo primero es lo primero: debemos decidir cómo se verá nuestro modelo de aplicación. Hacemos esto definiendo la forma de nuestros datos, datos a los que se podrá acceder en toda la aplicación.

Una buena regla general es que si los datos deben usarse en varios componentes de Redux, o deben durar mucho tiempo (como lo será nuestro token), entonces deben almacenarse en la base de datos. Por el contrario, si los datos son locales para el componente (como nuestros campos de nombre de usuario y contraseña), entonces deberían vivir como el estado del componente local y no almacenarse en la base de datos.

Vamos a crear nuestro modelo de base de datos y especificar nuestro token:

 (ns unearthing-clojurescript.state.db (:require [cljs.spec.alpha :as s] [re-frame.core :as re-frame])) (s/def ::token string?) (s/def ::db (s/keys :opt-un [::token])) (def default-db {:token nil})

Hay algunos puntos interesantes que vale la pena señalar aquí:

  • Usamos la biblioteca de spec de Clojure para describir cómo se supone que deben verse nuestros datos. Esto es especialmente apropiado en un lenguaje dinámico como Clojure[Script].
  • Para este ejemplo, solo hacemos un seguimiento de un token global que representará a nuestro usuario una vez que haya iniciado sesión. Este token es una cadena simple.
  • Sin embargo, antes de que el usuario inicie sesión, no tendremos un token. Esto está representado por la palabra clave :opt-un , que significa "opcional, no calificado". (En Clojure, una palabra clave regular sería algo así como :cat , mientras que una palabra clave calificada podría ser algo así como :animal/cat . La calificación normalmente se lleva a cabo en el nivel del módulo; esto evita que las palabras clave en diferentes módulos se golpeen entre sí).
  • Finalmente, especificamos el estado por defecto de nuestra base de datos, que es como se inicializa.

En cualquier momento, debemos estar seguros de que los datos de nuestra base de datos coinciden con nuestra especificación aquí.

Suscripciones

Ahora que hemos descrito nuestro modelo de datos, necesitamos reflejar cómo nuestra vista muestra esos datos. Ya hemos descrito cómo se ve nuestra vista en nuestro componente Redux; ahora simplemente necesitamos conectar nuestra vista a nuestra base de datos.

Con Redux, no accedemos a nuestra base de datos directamente; esto podría generar problemas de ciclo de vida y concurrencia. En cambio, registramos nuestra relación con una faceta de la base de datos a través de suscripciones .

Una suscripción le dice a Re-Frame (y Reagent) que dependemos de una parte de la base de datos, y si esa parte se altera, entonces nuestro componente Redux debe volver a renderizarse.

Las suscripciones son muy sencillas de definir:

 (ns unearthing-clojurescript.state.subs (:require [re-frame.core :refer [reg-sub]])) (reg-sub :token ; <- the name of the subscription (fn [{:keys [token] :as db} _] ; first argument is the database, second argument is any token)) ; args passed to the subscribe function (not used here)

Aquí, registramos una sola suscripción: al token mismo. Una suscripción es simplemente el nombre de la suscripción y la función que extrae ese elemento de la base de datos. Podemos hacer lo que queramos con ese valor y mutar la vista tanto como queramos aquí; sin embargo, en este caso, simplemente extraemos el token de la base de datos y lo devolvemos.

Hay mucho, mucho más que puede hacer con las suscripciones, como definir vistas en subsecciones de la base de datos para un alcance más estricto en la nueva representación, ¡pero lo mantendremos simple por ahora!

Eventos

Tenemos nuestra base de datos, y tenemos nuestra vista en la base de datos. ¡Ahora necesitamos desencadenar algunos eventos! En este ejemplo, tenemos dos tipos de eventos:

  • El evento puro ( sin efecto secundario) de escribir un nuevo token en la base de datos.
  • El evento de E/S ( que tiene un efecto secundario) de salir y solicitar nuestro token a través de alguna interacción con el cliente.

Empezaremos con la fácil. Re-frame incluso proporciona una función exactamente para este tipo de evento:

 (ns unearthing-clojurescript.state.events (:require [re-frame.core :refer [reg-event-db reg-event-fx reg-fx] :as rf] [unearthing-clojurescript.state.db :refer [default-db]])) ; our start up event that initialises the database. ; we'll trigger this in our core.cljs (reg-event-db :initialise-db (fn [_ _] default-db)) ; a simple event that places a token in the database (reg-event-db :store-login (fn [db [_ token]] (assoc db :token token)))

Nuevamente, es bastante sencillo aquí: hemos definido dos eventos. El primero es para inicializar nuestra base de datos. (¿Ves cómo ignora sus dos argumentos? ¡Siempre inicializamos la base de datos con nuestra base de datos default-db !) El segundo es para almacenar nuestro token una vez que lo tenemos.

Tenga en cuenta que ninguno de estos eventos tiene efectos secundarios: ¡no hay llamadas externas, ni E/S en absoluto! Esto es muy importante para preservar la santidad del sagrado proceso Redux. No lo hagas impuro para no desear la ira de Redux sobre ti.

Finalmente, necesitamos nuestro evento de inicio de sesión. Lo colocaremos debajo de los demás:

 (reg-event-fx :login (fn [{:keys [db]} [_ credentials]] {:request-token credentials})) (reg-fx :request-token (fn [{:keys [username password]}] (let [token (complicated-io-login-operation username password)] (rf/dispatch [:store-login token]))))

La función reg-event-fx es muy similar a reg-event-db , aunque existen algunas diferencias sutiles.

  • El primer argumento ya no es solo la base de datos en sí. Contiene una multitud de otras cosas que puede usar para administrar el estado de la aplicación.
  • El segundo argumento es muy parecido a reg-event-db .
  • En lugar de simplemente devolver el nuevo db , en su lugar devolvemos un mapa que representa todos los efectos ("fx") que deberían ocurrir para este evento. En este caso, simplemente llamamos al efecto :request-token , que se define a continuación. Uno de los otros efectos válidos es :dispatch , que simplemente llama a otro evento.

Una vez que se ha enviado nuestro efecto, se llama a nuestro efecto :request-token , que realiza nuestra operación de inicio de sesión de E/S de ejecución prolongada. Una vez que esto termina, felizmente despacha el resultado de regreso al ciclo de eventos, ¡completando así el ciclo!

Tutorial de ClojureScript: el resultado final

¡Entonces! Hemos definido nuestra abstracción de almacenamiento. ¿Cómo se ve el componente ahora?

 (ns unearthing-clojurescript.login (:require [reagent.core :as reagent :refer [atom]] [re-frame.core :as rf])) ;; -- STATE -- (def username (atom nil)) (def password (atom nil)) ;; -- VIEW -- (defn component [] [:div [:b "Username"] [:input {:type "text" :value @username :on-change #(reset! username (-> % .-target .-value))}] [:b "Password"] [:input {:type "password" :value @password :on-change #(reset! password (-> % .-target .-value))}] [:input {:type "button" :value "Login!" :on-click #(rf/dispatch [:login {:username @username :password @password]})}]])

Y nuestro componente de aplicación:

 (ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- VIEW -- (defn component [] [:div [login/component]])

Y por último, acceder a nuestro token en algún componente remoto es tan sencillo como:

 (let [token @(rf/subscribe [:token])] ; ... )

Poniendolo todo junto:

Cómo funcionan el estado local y el estado global (Redux) en el ejemplo de inicio de sesión.

Sin alboroto, sin desorden.

El desacoplamiento de componentes con Redux/Re-frame significa una gestión de estado limpia

Usando Redux (a través de un reencuadre), desacoplamos con éxito nuestros componentes de vista del desorden del manejo del estado. ¡Extender nuestra abstracción de estado ahora es pan comido!

Redux en ClojureScript realmente es así de fácil: no tienes excusa para no intentarlo.

Si está listo para ponerse manos a la obra, le recomiendo que consulte los fantásticos documentos de reencuadre y nuestro sencillo ejemplo práctico. Espero leer sus comentarios sobre este tutorial de ClojureScript a continuación. ¡La mejor de las suertes!

Relacionado: Administración de estado en Angular usando Firebase