Top Level Control mit Redux State Management: Ein ClojureScript-Tutorial

Veröffentlicht: 2022-03-11

Willkommen zurück zum zweiten spannenden Teil von Unearthing ClojureScript! In diesem Beitrag werde ich den nächsten großen Schritt behandeln, um ClojureScript ernst zu nehmen: Zustandsverwaltung – in diesem Fall mit React.

Bei Front-End-Software ist die Zustandsverwaltung eine große Sache. Out-of-the-Box gibt es mehrere Möglichkeiten, den Zustand in React zu handhaben:

  • Den Zustand auf der obersten Ebene halten und ihn (oder Handler für einen bestimmten Teil des Zustands) an untergeordnete Komponenten weitergeben.
  • Reinheit aus dem Fenster werfen und globale Variablen oder eine Lovecraftsche Form der Abhängigkeitsinjektion haben.

Im Allgemeinen sind beide nicht großartig. Es ist ziemlich einfach, den Status auf der obersten Ebene zu halten, aber dann gibt es eine Menge Overhead, um den Anwendungsstatus an jede Komponente weiterzugeben, die ihn benötigt.

Im Vergleich dazu kann das Vorhandensein globaler Variablen (oder anderer naiver Zustandsversionen) zu schwer nachvollziehbaren Nebenläufigkeitsproblemen führen, was dazu führt, dass Komponenten nicht aktualisiert werden, wenn Sie dies erwarten, oder umgekehrt.

Wie kann das also angegangen werden? Für diejenigen unter Ihnen, die mit React vertraut sind, haben Sie vielleicht Redux, einen Zustandscontainer für JavaScript-Apps, ausprobiert. Möglicherweise haben Sie dies aus eigenem Antrieb herausgefunden, als Sie mutig nach einem handhabbaren System zur Erhaltung des Zustands gesucht haben. Oder Sie sind vielleicht gerade darüber gestolpert, als Sie über JavaScript und andere Web-Tools gelesen haben.

Unabhängig davon, wie die Leute Redux betrachten, kommen sie meiner Erfahrung nach im Allgemeinen auf zwei Gedanken:

  • „Ich habe das Gefühl, dass ich das benutzen muss, weil alle sagen, dass ich es benutzen muss.“
  • „Ich verstehe nicht ganz, warum das besser ist.“

Im Allgemeinen bietet Redux eine Abstraktion, die die Zustandsverwaltung in die reaktive Natur von React einfügt. Indem Sie die gesamte Statefulness auf ein System wie Redux auslagern, bewahren Sie die Reinheit von React. So werden Sie am Ende viel weniger Kopfschmerzen haben und im Allgemeinen etwas, worüber Sie viel einfacher nachdenken können.

Für diejenigen, die neu bei Clojure sind

Während dies Ihnen vielleicht nicht hilft, ClojureScript ganz von Grund auf neu zu lernen, werde ich hier zumindest einige grundlegende Zustandskonzepte in Clojure[Script] zusammenfassen. Sie können diese Teile gerne überspringen, wenn Sie bereits ein erfahrener Clojurianer sind!

Erinnern Sie sich an eine der Clojure-Grundlagen, die auch für ClojureScript gilt: Standardmäßig sind Daten unveränderlich. Dies ist großartig, um zu entwickeln und Garantien zu haben, dass das, was Sie in Zeitschritt N erstellen, in Zeitschritt > N immer noch dasselbe ist. ClojureScript bietet uns auch eine bequeme Möglichkeit, über das atom einen veränderlichen Zustand zu haben, wenn wir ihn brauchen.

Ein atom in ClojureScript ist einer AtomicReference in Java sehr ähnlich: Es stellt ein neues Objekt bereit, das seinen Inhalt mit Parallelitätsgarantien sperrt. Genau wie in Java können Sie in diesem Objekt alles platzieren, was Sie wollen – von da an ist dieses Atom eine atomare Referenz auf alles, was Sie wollen.

Sobald Sie Ihr atom haben, können Sie ihm atomar einen neuen Wert setzen, indem Sie den reset! Funktion (beachten Sie das ! in der Funktion – in der Clojure-Sprache wird dies oft verwendet, um anzuzeigen, dass eine Operation zustandsbehaftet oder unrein ist).

Beachten Sie auch, dass es Clojure – im Gegensatz zu Java – egal ist, was Sie in Ihr atom . Es kann ein String, eine Liste oder ein Objekt sein. Dynamisches Tippen, Baby!

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

Reagenz erweitert dieses Atomkonzept um ein eigenes atom . (Wenn Sie mit Reagent nicht vertraut sind, sehen Sie sich den Beitrag davor an.) Dies verhält sich identisch mit dem ClojureScript- atom , außer dass es auch Render-Ereignisse in Reagent auslöst, genau wie der eingebaute Zustandsspeicher von React.

Ein Beispiel:

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

Dies zeigt ein einzelnes <div> mit einem <span> mit der Aufschrift „Hello, world!“ und eine Schaltfläche, wie Sie vielleicht erwarten. Durch Drücken dieser Taste wird my-atom atomar mutiert, sodass es "there!" enthält. . Dadurch wird ein Neuzeichnen der Komponente ausgelöst, was dazu führt, dass die Spanne „Hallo, da!“ sagt. stattdessen.

Überblick über die Zustandsverwaltung, wie sie von Redux und Reagent gehandhabt wird.

Dies scheint für eine lokale Mutation auf Komponentenebene einfach genug zu sein, aber was ist, wenn wir eine kompliziertere Anwendung mit mehreren Abstraktionsebenen haben? Oder wenn wir einen gemeinsamen Zustand zwischen mehreren Unterkomponenten und ihren Unterkomponenten teilen müssen?

Ein komplizierteres Beispiel

Lassen Sie uns dies anhand eines Beispiels untersuchen. Hier werden wir eine grobe Anmeldeseite implementieren:

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

Wir werden diese Login-Komponente dann in unserer Haupt- app.cljs wie folgt hosten:

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

Der erwartete Arbeitsablauf ist somit:

  1. Wir warten darauf, dass der Benutzer seinen Benutzernamen und sein Passwort eingibt und auf „Senden“ klickt.
  2. Dies löst unsere do-login-io Funktion in der übergeordneten Komponente aus.
  3. Die do-login-io Funktion führt einige E/A-Operationen aus (z. B. das Anmelden bei einem Server und das Abrufen eines Tokens).

Wenn diese Operation blockiert, haben wir bereits einen Haufen Ärger, da unsere Anwendung eingefroren ist – wenn nicht, müssen wir uns um Async kümmern!

Außerdem müssen wir dieses Token jetzt allen unseren Unterkomponenten zur Verfügung stellen, die Anfragen an unseren Server stellen möchten. Code-Refaktorisierung ist jetzt viel schwieriger!

Schließlich ist unsere Komponente jetzt nicht mehr rein reaktiv – sie ist jetzt an der Verwaltung des Zustands der restlichen Anwendung beteiligt, löst E/A aus und ist im Allgemeinen ein wenig lästig.

ClojureScript-Tutorial: Geben Sie Redux ein

Redux ist der Zauberstab, der all Ihre staatlichen Träume wahr werden lässt. Richtig implementiert, bietet es eine State-Sharing-Abstraktion, die sicher, schnell und einfach zu verwenden ist.

Das Innenleben von Redux (und die Theorie dahinter) liegt etwas außerhalb des Rahmens dieses Artikels. Stattdessen werde ich in ein funktionierendes Beispiel mit ClojureScript eintauchen, das hoffentlich zeigen sollte, wozu es in der Lage ist!

In unserem Kontext wird Redux durch eine der vielen verfügbaren ClojureScript-Bibliotheken implementiert; dieser namens re-frame. Es bietet einen Clojure-ifizierten Wrapper um Redux, der es (meiner Meinung nach) zu einem absoluten Vergnügen macht, es zu verwenden.

Die Grundlagen

Redux hebt Ihren Anwendungsstatus hervor und lässt Ihre Komponenten leichtgewichtig. Eine reduxifizierte Komponente muss sich nur Gedanken über Folgendes machen:

  • Wie es aussieht
  • Welche Daten es verbraucht
  • Welche Ereignisse es auslöst

Der Rest wird hinter den Kulissen abgewickelt.

Um diesen Punkt zu betonen, lassen Sie uns unsere obige Anmeldeseite reduxifizieren.

Die Datenbank

Das Wichtigste zuerst: Wir müssen entscheiden, wie unser Anwendungsmodell aussehen soll. Wir tun dies, indem wir die Form unserer Daten definieren, Daten, auf die in der gesamten App zugegriffen werden kann.

Eine gute Faustregel lautet: Wenn die Daten über mehrere Redux-Komponenten hinweg verwendet werden müssen oder langlebig sein müssen (wie es unser Token sein wird), sollten sie in der Datenbank gespeichert werden. Wenn die Daten dagegen lokal für die Komponente sind (z. B. unsere Benutzernamen- und Passwortfelder), sollten sie als lokaler Komponentenstatus existieren und nicht in der Datenbank gespeichert werden.

Lassen Sie uns unsere Datenbank-Boilerplate erstellen und unser Token spezifizieren:

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

Hier sind einige interessante Punkte zu beachten:

  • Wir verwenden die spec von Clojure, um zu beschreiben , wie unsere Daten aussehen sollen. Dies ist besonders in einer dynamischen Sprache wie Clojure[Script] angebracht.
  • In diesem Beispiel verfolgen wir nur ein globales Token, das unseren Benutzer darstellt, sobald er sich angemeldet hat. Dieses Token ist eine einfache Zeichenfolge.
  • Bevor sich der Benutzer jedoch anmeldet, haben wir kein Token. Dies wird durch das Schlüsselwort :opt-un dargestellt, das für „optional, nicht qualifiziert“ steht. (In Clojure wäre ein reguläres Schlüsselwort so etwas wie :cat , während ein qualifiziertes Schlüsselwort so etwas wie :animal/cat sein könnte. Die Qualifizierung findet normalerweise auf Modulebene statt – dies verhindert, dass sich Schlüsselwörter in verschiedenen Modulen gegenseitig überfordern.)
  • Schließlich geben wir den Standardzustand unserer Datenbank an, wie sie initialisiert wird.

Wir sollten uns zu jedem Zeitpunkt darauf verlassen können, dass die Daten in unserer Datenbank mit unseren Spezifikationen hier übereinstimmen.

Abonnements

Nachdem wir nun unser Datenmodell beschrieben haben, müssen wir reflektieren, wie unsere Ansicht diese Daten darstellt. Wie unser View in unserer Redux-Komponente aussieht, haben wir bereits beschrieben – jetzt müssen wir unseren View nur noch mit unserer Datenbank verbinden.

Mit Redux greifen wir nicht direkt auf unsere Datenbank zu – dies könnte zu Lebenszyklus- und Parallelitätsproblemen führen. Stattdessen registrieren wir unsere Beziehung mit einer Facette der Datenbank durch Abonnements .

Ein Abonnement teilt re-frame (und Reagent) mit, dass wir von einem Teil der Datenbank abhängen, und wenn dieser Teil geändert wird, dann sollte unsere Redux-Komponente neu gerendert werden.

Abonnements sind sehr einfach zu definieren:

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

Hier registrieren wir ein einzelnes Abonnement – ​​für das Token selbst. Ein Abonnement ist einfach der Name des Abonnements und die Funktion, die dieses Element aus der Datenbank extrahiert. Wir können mit diesem Wert machen, was wir wollen, und die Ansicht so verändern, wie wir wollen; In diesem Fall extrahieren wir jedoch einfach das Token aus der Datenbank und geben es zurück.

Es gibt noch viel, viel mehr, was Sie mit Abonnements tun können – wie das Definieren von Ansichten auf Unterabschnitte der Datenbank für einen engeren Bereich beim erneuten Rendern – aber wir halten es vorerst einfach!

Veranstaltungen

Wir haben unsere Datenbank und wir haben unsere Einsicht in die Datenbank. Jetzt müssen wir einige Ereignisse auslösen! In diesem Beispiel haben wir zwei Arten von Ereignissen:

  • Das reine Ereignis ( ohne Nebenwirkungen) des Schreibens eines neuen Tokens in die Datenbank.
  • Das E/A-Ereignis ( mit einem Nebeneffekt) beim Ausgehen und Anfordern unseres Tokens durch eine Client-Interaktion.

Wir fangen mit dem Einfachen an. Re-Frame bietet sogar eine Funktion genau für diese Art von Ereignis:

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

Auch hier ist es ziemlich einfach – wir haben zwei Ereignisse definiert. Der erste dient der Initialisierung unserer Datenbank. (Sehen Sie, wie es beide Argumente ignoriert? Wir initialisieren die Datenbank immer mit unserer default-db !) Die zweite dient zum Speichern unseres Tokens, sobald wir es haben.

Beachten Sie, dass keines dieser Ereignisse Nebeneffekte hat – keine externen Aufrufe, überhaupt keine I/O! Dies ist sehr wichtig, um die Heiligkeit des heiligen Redux-Prozesses zu bewahren. Machen Sie es nicht unrein, damit Sie nicht den Zorn von Redux auf sich ziehen wollen.

Schließlich brauchen wir noch unser Login-Event. Wir platzieren es unter den anderen:

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

Die Funktion reg-event-fx ähnelt weitgehend reg-event-db , obwohl es einige subtile Unterschiede gibt.

  • Das erste Argument ist nicht mehr nur die Datenbank selbst. Es enthält eine Vielzahl anderer Dinge, die Sie zum Verwalten des Anwendungsstatus verwenden können.
  • Das zweite Argument ist ähnlich wie in reg-event-db .
  • Anstatt nur die neue db zurückzugeben, geben wir stattdessen eine Karte zurück, die alle Effekte („fx“) darstellt, die für dieses Ereignis auftreten sollten. In diesem Fall nennen wir einfach den :request-token Effekt, der unten definiert wird. Einer der anderen gültigen Effekte ist :dispatch , der einfach ein anderes Ereignis aufruft.

Sobald unser Effekt versendet wurde, wird unser :request-token Effekt aufgerufen, der unsere lang andauernde I/O-Login-Operation durchführt. Sobald dies abgeschlossen ist, sendet es das Ergebnis glücklich zurück in die Ereignisschleife, wodurch der Zyklus abgeschlossen wird!

ClojureScript-Tutorial: Das Endergebnis

Damit! Wir haben unsere Speicherabstraktion definiert. Wie sieht das Bauteil jetzt aus?

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

Und unsere App-Komponente:

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

Und schließlich ist der Zugriff auf unser Token in einer Remote-Komponente so einfach wie:

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

Alles zusammen:

Wie lokaler Zustand und globaler (Redux) Zustand im Anmeldebeispiel funktionieren.

Keine Aufregung, kein Muss.

Das Entkoppeln von Komponenten mit Redux/Reframe bedeutet eine saubere Zustandsverwaltung

Mit Redux (via Re-Frame) haben wir unsere Ansichtskomponenten erfolgreich von dem Durcheinander der Zustandsbehandlung entkoppelt. Die Erweiterung unserer Zustandsabstraktion ist jetzt ein Kinderspiel!

Redux in ClojureScript ist wirklich so einfach – Sie haben keine Entschuldigung, es nicht zu versuchen.

Wenn Sie bereit sind, loszulegen, empfehle ich Ihnen, sich die fantastischen Re-Frame-Dokumente und unser einfaches, ausgearbeitetes Beispiel anzusehen. Ich freue mich darauf, Ihre Kommentare zu diesem ClojureScript-Tutorial unten zu lesen. Viel Glück!

Verwandte: Zustandsverwaltung in Angular mit Firebase