Contrôle de niveau supérieur avec gestion de l'état Redux : un didacticiel ClojureScript

Publié: 2022-03-11

Bienvenue pour le deuxième épisode passionnant de Unearthing ClojureScript ! Dans cet article, je vais couvrir la prochaine grande étape pour devenir sérieux avec ClojureScript : la gestion de l'état, dans ce cas, en utilisant React.

Avec le logiciel frontal, la gestion de l'état est un gros problème. Prêt à l'emploi, il existe plusieurs façons de gérer l'état dans React :

  • Garder l'état au niveau supérieur et le transmettre (ou les gestionnaires d'un état particulier) aux composants enfants.
  • Jeter la pureté par la fenêtre et avoir des variables globales ou une forme lovecraftienne d'injection de dépendance.

D'une manière générale, aucun de ceux-ci n'est génial. Garder l'état au niveau supérieur est assez simple, mais il y a alors une grande surcharge pour transmettre l'état de l'application à chaque composant qui en a besoin.

En comparaison, le fait d'avoir des variables globales (ou d'autres versions naïves de l'état) peut entraîner des problèmes de concurrence difficiles à tracer, entraînant la non mise à jour des composants lorsque vous vous y attendez, ou vice versa.

Alors, comment cela peut-il être résolu? Pour ceux d'entre vous qui connaissent React, vous avez peut-être essayé Redux, un conteneur d'état pour les applications JavaScript. Vous avez peut-être découvert cela de votre propre gré, en cherchant hardiment un système gérable pour maintenir l'état. Ou vous venez peut-être de tomber dessus en lisant sur JavaScript et d'autres outils Web.

Quelle que soit la façon dont les gens finissent par regarder Redux, d'après mon expérience, ils se retrouvent généralement avec deux pensées :

  • "Je sens que je dois l'utiliser parce que tout le monde dit que je dois l'utiliser."
  • "Je ne comprends pas vraiment pourquoi c'est mieux."

De manière générale, Redux fournit une abstraction qui permet à la gestion des états de s'inscrire dans la nature réactive de React. En déchargeant tous les états sur un système comme Redux, vous préservez la pureté de React. Ainsi, vous vous retrouverez avec beaucoup moins de maux de tête et généralement quelque chose sur lequel il est beaucoup plus facile de raisonner.

Pour ceux qui découvrent Clojure

Bien que cela ne vous aide pas à apprendre entièrement ClojureScript à partir de rien, je vais au moins récapituler ici certains concepts d'état de base dans Clojure[Script]. N'hésitez pas à sauter ces parties si vous êtes déjà un Clojurien chevronné !

Rappelez-vous l'une des bases de Clojure qui s'applique également à ClojureScript : par défaut, les données sont immuables. C'est idéal pour développer et avoir des garanties que ce que vous créez au pas de temps N est toujours le même au pas de temps > N. ClojureScript nous fournit également un moyen pratique d'avoir un état mutable si nous en avons besoin, via le concept atom .

Un atom dans ClojureScript est très similaire à un AtomicReference en Java : il fournit un nouvel objet qui verrouille son contenu avec des garanties de concurrence. Tout comme en Java, vous pouvez placer tout ce que vous voulez dans cet objet - à partir de là, cet atome sera une référence atomique à tout ce que vous voulez.

Une fois que vous avez votre atom , vous pouvez y définir une nouvelle valeur de manière atomique en utilisant la reset! fonction (notez le ! dans la fonction - dans le langage Clojure, cela est souvent utilisé pour signifier qu'une opération est avec état ou impure).

Notez également que, contrairement à Java, Clojure ne se soucie pas de ce que vous mettez dans votre atom . Il peut s'agir d'une chaîne, d'une liste ou d'un objet. Dactylographie dynamique, bébé !

 (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 :)

Le réactif étend ce concept d'atome avec son propre atom . (Si vous n'êtes pas familier avec Reagent, consultez le post avant cela.) Cela se comporte de la même manière que l' atom ClojureScript, sauf qu'il déclenche également des événements de rendu dans Reagent, tout comme le magasin d'état intégré de React.

Un exemple:

 (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!")}]])

Cela affichera un seul <div> contenant un <span> disant "Hello, world!" et un bouton, comme vous vous en doutez. Appuyer sur ce bouton va muter atomiquement my-atom pour contenir "there!" . Cela déclenchera un redessin du composant, ce qui fera dire à la plage "Hello, there!" au lieu.

Vue d'ensemble de la gestion de l'état telle qu'elle est gérée par Redux et Reagent.

Cela semble assez simple pour une mutation locale au niveau des composants, mais que se passe-t-il si nous avons une application plus compliquée qui a plusieurs niveaux d'abstraction ? Ou si nous avons besoin de partager un état commun entre plusieurs sous-composants et leurs sous-composants ?

Un exemple plus compliqué

Explorons cela avec un exemple. Ici, nous allons implémenter une page de connexion brute :

 (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)}]])

Nous hébergerons ensuite ce composant de connexion dans notre principal app.cljs , comme ceci :

 (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]])

Le flux de travail attendu est donc :

  1. Nous attendons que l'utilisateur entre son nom d'utilisateur et son mot de passe et clique sur Soumettre.
  2. Cela déclenchera notre fonction do-login-io dans le composant parent.
  3. La fonction do-login-io effectue certaines opérations d'E/S (telles que la connexion à un serveur et la récupération d'un jeton).

Si cette opération est bloquante, alors nous sommes déjà dans un tas de problèmes, car notre application est gelée - si ce n'est pas le cas, alors nous devons nous soucier de l'async !

De plus, nous devons maintenant fournir ce jeton à tous nos sous-composants qui souhaitent effectuer des requêtes sur notre serveur. La refactorisation du code est devenue beaucoup plus difficile !

Enfin, notre composant n'est plus purement réactif , il est désormais complice de la gestion de l'état du reste de l'application, déclenchant des E/S et étant généralement un peu gênant.

Tutoriel ClojureScript : Entrez dans Redux

Redux est la baguette magique qui réalise tous vos rêves basés sur l'état. Correctement implémenté, il fournit une abstraction de partage d'état sûre, rapide et facile à utiliser.

Le fonctionnement interne de Redux (et la théorie qui le sous-tend) sort quelque peu du cadre de cet article. Au lieu de cela, je vais plonger dans un exemple de travail avec ClojureScript, qui devrait, espérons-le, démontrer de quoi il est capable !

Dans notre contexte, Redux est implémenté par l'une des nombreuses bibliothèques ClojureScript disponibles ; celui-ci appelé re-frame. Il fournit un wrapper Clojure-ified autour de Redux qui (à mon avis) en fait un plaisir absolu à utiliser.

Les bases

Redux relève l'état de votre application, laissant vos composants légers. Un composant Reduxified n'a qu'à penser à :

  • À quoi il ressemble
  • Quelles données il consomme
  • Quels événements cela déclenche-t-il ?

Le reste est géré en coulisses.

Pour souligner ce point, reduxifions notre page de connexion ci-dessus.

La base de données

Tout d'abord, nous devons décider à quoi ressemblera notre modèle d'application. Pour ce faire, nous définissons la forme de nos données, données qui seront accessibles dans toute l'application.

Une bonne règle empirique est que si les données doivent être utilisées sur plusieurs composants Redux, ou doivent durer longtemps (comme le sera notre jeton), alors elles doivent être stockées dans la base de données. En revanche, si les données sont locales au composant (comme nos champs de nom d'utilisateur et de mot de passe), elles doivent vivre en tant qu'état de composant local et ne pas être stockées dans la base de données.

Créons notre passe-partout de base de données et spécifions notre jeton :

 (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})

Il y a quelques points intéressants à noter ici :

  • Nous utilisons la bibliothèque de spec de Clojure pour décrire à quoi nos données sont censées ressembler. Ceci est particulièrement approprié dans un langage dynamique comme Clojure[Script].
  • Pour cet exemple, nous gardons uniquement la trace d'un jeton global qui représentera notre utilisateur une fois qu'il se sera connecté. Ce jeton est une simple chaîne.
  • Cependant, avant que l'utilisateur ne se connecte, nous n'aurons pas de jeton. Ceci est représenté par le mot-clé :opt-un , qui signifie « facultatif, non qualifié ». (Dans Clojure, un mot-clé normal serait quelque chose comme :cat , tandis qu'un mot-clé qualifié pourrait être quelque chose comme :animal/cat . La qualification a normalement lieu au niveau du module, ce qui empêche les mots-clés de différents modules de s'écraser les uns les autres.)
  • Enfin, nous spécifions l'état par défaut de notre base de données, c'est-à-dire la façon dont elle est initialisée.

À tout moment, nous devons être sûrs que les données de notre base de données correspondent à nos spécifications ici.

Abonnements

Maintenant que nous avons décrit notre modèle de données, nous devons refléter la manière dont notre vue affiche ces données. Nous avons déjà décrit à quoi ressemble notre vue dans notre composant Redux - il nous suffit maintenant de connecter notre vue à notre base de données.

Avec Redux, nous n'accédons pas directement à notre base de données, ce qui pourrait entraîner des problèmes de cycle de vie et de concurrence. Au lieu de cela, nous enregistrons notre relation avec une facette de la base de données via des abonnements .

Un abonnement indique à re-frame (et Reagent) que nous dépendons d'une partie de la base de données, et si cette partie est modifiée, alors notre composant Redux doit être rendu à nouveau.

Les abonnements sont très simples à définir :

 (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)

Ici, nous enregistrons un seul abonnement, au jeton lui-même. Un abonnement est simplement le nom de l'abonnement et la fonction qui extrait cet élément de la base de données. Nous pouvons faire ce que nous voulons pour cette valeur et modifier la vue autant que nous le souhaitons ici ; cependant, dans ce cas, nous extrayons simplement le jeton de la base de données et le renvoyons.

Il y a beaucoup, beaucoup plus que vous pouvez faire avec les abonnements, comme définir des vues sur des sous-sections de la base de données pour une portée plus étroite sur le re-rendu, mais nous allons rester simple pour l'instant !

Événements

Nous avons notre base de données et nous avons notre point de vue sur la base de données. Maintenant, nous devons déclencher des événements ! Dans cet exemple, nous avons deux types d'événements :

  • L'événement pur ( sans effet secondaire) d'écriture d'un nouveau jeton dans la base de données.
  • L'événement d'E/S ( ayant un effet secondaire) consistant à sortir et à demander notre jeton via une interaction client.

Nous allons commencer par le plus facile. Re-frame fournit même une fonction exactement pour ce genre d'événement :

 (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)))

Encore une fois, c'est assez simple ici : nous avons défini deux événements. Le premier sert à initialiser notre base de données. (Vous voyez comment il ignore ses deux arguments ? Nous initialisons toujours la base de données avec notre default-db !) Le second sert à stocker notre jeton une fois que nous l'avons.

Notez qu'aucun de ces événements n'a d'effets secondaires : pas d'appels externes, pas d'E/S du tout ! Ceci est très important pour préserver le caractère sacré du processus sacré de Redux. Ne le rendez pas impur de peur de souhaiter la colère de Redux sur vous.

Enfin, nous avons besoin de notre événement de connexion. Nous le placerons sous les autres :

 (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 fonction reg-event-fx est largement similaire à reg-event-db , bien qu'il existe quelques différences subtiles.

  • Le premier argument n'est plus seulement la base de données elle-même. Il contient une multitude d'autres éléments que vous pouvez utiliser pour gérer l'état de l'application.
  • Le deuxième argument ressemble beaucoup à reg-event-db .
  • Plutôt que de simplement renvoyer le nouveau db , nous renvoyons à la place une carte qui représente tous les effets ("fx") qui devraient se produire pour cet événement. Dans ce cas, nous appelons simplement l'effet :request-token , qui est défini ci-dessous. L'un des autres effets valides est :dispatch , qui appelle simplement un autre événement.

Une fois que notre effet a été distribué, notre effet :request-token est appelé, ce qui exécute notre opération de connexion d'E/S de longue durée. Une fois que cela est terminé, il envoie joyeusement le résultat dans la boucle d'événements, complétant ainsi le cycle !

Tutoriel ClojureScript : le résultat final

Alors! Nous avons défini notre abstraction de stockage. À quoi ressemble le composant maintenant ?

 (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]})}]])

Et notre composant d'application :

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

Et enfin, accéder à notre jeton dans un composant distant est aussi simple que :

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

Mettre tous ensemble:

Fonctionnement de l'état local et de l'état global (Redux) dans l'exemple de connexion.

Pas de chichi, pas de désordre.

Le découplage des composants avec Redux/Reframe signifie une gestion d'état propre

En utilisant Redux (via le recadrage), nous avons réussi à découpler nos composants de vue du gâchis de la gestion des états. L'extension de notre abstraction d'état est maintenant un jeu d'enfant !

Redux dans ClojureScript est vraiment aussi simple que cela - vous n'avez aucune excuse pour ne pas l'essayer.

Si vous êtes prêt à craquer, je vous recommande de consulter les fantastiques documents de recadrage et notre exemple simple. J'ai hâte de lire vos commentaires sur ce tutoriel ClojureScript ci-dessous. Bonne chance!

En relation: Gestion des états dans Angular à l'aide de Firebase