Control de nivel superior cu managementul stării Redux: un tutorial ClojureScript

Publicat: 2022-03-11

Bine ați revenit pentru cea de-a doua parte captivantă a lui Unearthing ClojureScript! În această postare, voi acoperi următorul pas mare pentru a deveni serios cu ClojureScript: managementul stării — în acest caz, folosind React.

Cu software-ul front-end, managementul de stat este o mare problemă. Ieșit din cutie, există câteva moduri de a gestiona starea în React:

  • Menținerea stării la nivelul superior și transmiterea acesteia (sau handlere pentru o anumită parte a stării) către componentele copil.
  • Aruncarea purității pe fereastră și având variabile globale sau vreo formă Lovecraftiană de injectare a dependenței.

În general, niciunul dintre acestea nu este grozav. Păstrarea stării la nivelul superior este destul de simplă, dar apoi există o cantitate mare de cheltuieli generale pentru a transmite starea aplicației către fiecare componentă care are nevoie de ea.

Prin comparație, a avea variabile globale (sau alte versiuni naive de stare) poate duce la probleme de concurență greu de urmărit, ceea ce duce la neactualizarea componentelor atunci când vă așteptați, sau invers.

Deci, cum poate fi abordat acest lucru? Pentru cei dintre voi care sunt familiarizați cu React, este posibil să fi încercat Redux, un container de stare pentru aplicațiile JavaScript. Poate că ați găsit acest lucru din propria voință, căutând cu îndrăzneală un sistem gestionabil pentru menținerea stării. Sau este posibil să fi dat peste el în timp ce citiți despre JavaScript și alte instrumente web.

Indiferent de modul în care oamenii ajung să se uite la Redux, din experiența mea, în general, ajung să aibă două gânduri:

  • „Simt că trebuie să-l folosesc pentru că toată lumea spune că trebuie să-l folosesc.”
  • „Nu înțeleg pe deplin de ce este mai bine.”

În general, Redux oferă o abstractizare care permite managementului de stat să se încadreze în natura reactivă a React. Prin descărcarea întregii stări într-un sistem precum Redux, păstrați puritatea React. Astfel, veți ajunge cu mult mai puține dureri de cap și, în general, cu ceva despre care este mult mai ușor de gândit.

Pentru cei noi la Clojure

Deși acest lucru s-ar putea să nu vă ajute să învățați ClojureScript complet de la zero, aici voi recapitula cel puțin câteva concepte de bază de stare în Clojure[Script]. Simțiți-vă liber să omiteți aceste părți dacă sunteți deja un Clojurian experimentat!

Amintiți-vă una dintre elementele de bază Clojure care se aplică și ClojureScript: implicit, datele sunt imuabile. Acest lucru este grozav pentru dezvoltarea și pentru a avea garanții că ceea ce creați în intervalul de timp N este în continuare același la pasul de timp > N. ClojureScript ne oferă, de asemenea, o modalitate convenabilă de a avea o stare mutabilă dacă avem nevoie, prin intermediul conceptului de atom .

Un atom din ClojureScript este foarte asemănător cu un AtomicReference din Java: oferă un nou obiect care își blochează conținutul cu garanții de concurență. La fel ca în Java, puteți plasa orice doriți în acest obiect - de atunci, atomul respectiv va fi o referință atomică la orice doriți.

Odată ce aveți atom dvs., puteți seta atomic o nouă valoare în el folosind reset! funcția (notați ! în funcție — în limbajul Clojure, aceasta este adesea folosită pentru a semnifica faptul că o operație este cu stare sau impură).

De asemenea, rețineți că, spre deosebire de Java, lui Clojure nu îi pasă ce puneți în atom dvs. Poate fi un șir, o listă sau un obiect. Tastare dinamică, iubito!

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

Reactiv extinde acest concept de atom cu propriul său atom . (Dacă nu sunteți familiarizat cu Reagent, consultați postarea înainte de aceasta.) Acest lucru se comportă identic cu atom ClojureScript, cu excepția faptului că declanșează și evenimente de randare în Reagent, la fel ca magazinul de stare încorporat al React.

Un exemplu:

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

Aceasta va afișa un singur <div> care conține un <span> care spune „Bună, lume!” și un buton, așa cum v-ați aștepta. Dacă apăsați butonul respectiv, atomul my-atom va muta atomic pentru a conține "there!" . Aceasta va declanșa o redesenare a componentei, rezultând în intervalul să spună „Bună, acolo!” in schimb.

Prezentare generală a managementului de stat, așa cum este gestionat de Redux și Reagent.

Acest lucru pare destul de simplu pentru mutația locală la nivel de componentă, dar ce se întâmplă dacă avem o aplicație mai complicată care are mai multe niveluri de abstractizare? Sau dacă trebuie să împărtășim starea comună între mai multe sub-componente și sub-componentele lor?

Un exemplu mai complicat

Să explorăm asta cu un exemplu. Aici vom implementa o pagină brută de conectare:

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

Apoi vom găzdui această componentă de conectare în principalul nostru app.cljs , astfel:

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

Fluxul de lucru așteptat este astfel:

  1. Așteptăm ca utilizatorul să-și introducă numele de utilizator și parola și să apese pe trimitere.
  2. Acest lucru va declanșa funcția noastră do-login-io în componenta părinte.
  3. Funcția do-login-io efectuează unele operațiuni I/O (cum ar fi conectarea pe un server și preluarea unui token).

Dacă această operațiune se blochează, atunci suntem deja într-o grămadă de probleme, deoarece aplicația noastră este înghețată; dacă nu este, atunci avem de a ne face griji!

În plus, acum trebuie să furnizăm acest token tuturor sub-componentelor noastre care doresc să facă interogări către serverul nostru. Refactorizarea codului a devenit mult mai dificilă!

În cele din urmă, componenta noastră nu mai este acum pur reactivă – este acum complice la gestionarea stării restului aplicației, declanșând I/O și, în general, este un pic deranjant.

Tutorial ClojureScript: Introduceți Redux

Redux este bagheta magică care face ca toate visele tale bazate pe stare să devină realitate. Implementat corect, oferă o abstractizare de partajare a stării care este sigură, rapidă și ușor de utilizat.

Funcțiile interioare ale Redux (și teoria din spatele acestuia) sunt oarecum în afara domeniului de aplicare al acestui articol. În schimb, mă voi scufunda într-un exemplu de lucru cu ClojureScript, care, sperăm, ar trebui să demonstreze de ce este capabil!

În contextul nostru, Redux este implementat de una dintre numeroasele biblioteci ClojureScript disponibile; acesta numit re-frame. Oferă un înveliș Clojure-ified în jurul Redux care (în opinia mea) îl face o încântare absolută de utilizat.

Cele elementare

Redux ridică starea aplicației dvs., lăsând componentele ușoare. O componentă Reduxificată trebuie să se gândească doar la:

  • Cum arată
  • Ce date consumă
  • Ce evenimente declanșează

Restul se ocupă în culise.

Pentru a sublinia acest punct, haideți să reduxificăm pagina noastră de conectare de mai sus.

Baza de date

În primul rând, trebuie să decidem cum va arăta modelul nostru de aplicație. Facem acest lucru prin definirea formei datelor noastre, date care vor fi accesibile în întreaga aplicație.

O regulă generală bună este că, dacă datele trebuie utilizate în mai multe componente Redux sau trebuie să aibă o viață lungă (cum va fi simbolul nostru), atunci ar trebui să fie stocate în baza de date. În schimb, dacă datele sunt locale pentru componentă (cum ar fi câmpurile noastre de nume de utilizator și parolă), atunci ar trebui să trăiască ca starea componentei locale și să nu fie stocate în baza de date.

Să creăm baza noastră de date și să specificăm simbolul nostru:

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

Există câteva puncte interesante care merită remarcate aici:

  • Folosim biblioteca de spec pentru a descrie cum ar trebui să arate datele noastre. Acest lucru este adecvat în special într-un limbaj dinamic precum Clojure[Script].
  • Pentru acest exemplu, urmărim doar un simbol global care va reprezenta utilizatorul nostru odată ce acesta s-a conectat. Acest simbol este un șir simplu.
  • Cu toate acestea, înainte ca utilizatorul să se conecteze, nu vom avea un simbol. Acesta este reprezentat de cuvântul cheie :opt-un , care înseamnă „opțional, necalificat”. (În Clojure, un cuvânt cheie obișnuit ar fi ceva de genul :cat , în timp ce un cuvânt cheie calificat ar putea fi ceva de genul :animal/cat . În mod normal, calificarea are loc la nivel de modul - acest lucru împiedică cuvintele cheie din diferite module să se încurce reciproc.)
  • În cele din urmă, specificăm starea implicită a bazei noastre de date, care este modul în care este inițializată.

În orice moment, ar trebui să fim încrezători că datele din baza noastră de date se potrivesc cu specificațiile noastre de aici.

Abonamente

Acum că am descris modelul nostru de date, trebuie să reflectăm modul în care punctul nostru de vedere arată acele date. Am descris deja cum arată vizualizarea noastră în componenta noastră Redux - acum trebuie pur și simplu să ne conectăm vizualizarea la baza noastră de date.

Cu Redux, nu accesăm direct baza noastră de date - acest lucru ar putea duce la probleme legate de ciclul de viață și de concurență. În schimb, ne înregistrăm relația cu o fațetă a bazei de date prin abonamente .

Un abonament spune re-frame-ului (și reactivului) că depindem de o parte a bazei de date și, dacă acea parte este modificată, atunci componenta noastră Redux ar trebui să fie redată din nou.

Abonamentele sunt foarte simplu de definit:

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

Aici, înregistrăm un singur abonament — la simbolul în sine. Un abonament este pur și simplu numele abonamentului și funcția care extrage acel articol din baza de date. Putem face tot ce vrem la această valoare și putem modifica viziunea cât ne place aici; totuși, în acest caz, pur și simplu extragem jetonul din baza de date și îl returnăm.

Există multe, mult mai multe lucruri pe care le puteți face cu abonamentele — cum ar fi definirea vederilor asupra subsecțiunilor bazei de date pentru o sferă mai restrânsă de redare — dar o vom menține simplă pentru moment!

Evenimente

Avem baza noastră de date și avem viziunea noastră în baza de date. Acum trebuie să declanșăm câteva evenimente! În acest exemplu, avem două tipuri de evenimente:

  • Evenimentul pur ( fără efect secundar) al scrierii unui nou token în baza de date.
  • Evenimentul I/O ( care are un efect secundar) de a ieși și de a solicita token-ul nostru printr-o interacțiune cu clientul.

Vom începe cu cel ușor. Re-frame oferă chiar și o funcție exact pentru acest tip de eveniment:

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

Din nou, aici este destul de simplu – am definit două evenimente. Primul este pentru inițializarea bazei de date. (Vedeți cum ignoră ambele argumente? Inițializam întotdeauna baza de date cu default-db !) Al doilea este pentru stocarea jetonului nostru odată ce îl avem.

Observați că niciunul dintre aceste evenimente nu are efecte secundare - nici apeluri externe, nici I/O deloc! Acest lucru este foarte important pentru a păstra sfințenia procesului sfânt Redux. Nu-l impurificați ca nu cumva să doriți mânia lui Redux asupra voastră.

În cele din urmă, avem nevoie de evenimentul nostru de conectare. O vom plasa sub celelalte:

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

Funcția reg-event-fx este în mare măsură similară cu reg-event-db , deși există unele diferențe subtile.

  • Primul argument nu mai este doar baza de date în sine. Conține o multitudine de alte lucruri pe care le puteți utiliza pentru gestionarea stării aplicației.
  • Al doilea argument seamănă mult cu reg-event-db .
  • În loc să returnăm doar noul db , în schimb returnăm o hartă care reprezintă toate efectele („fx”) care ar trebui să se întâmple pentru acest eveniment. În acest caz, numim pur și simplu efectul :request-token , care este definit mai jos. Unul dintre celelalte efecte valide este :dispatch , care apelează pur și simplu un alt eveniment.

Odată ce efectul nostru a fost expediat, este apelat efectul nostru :request-token , care efectuează operația noastră de conectare I/O de lungă durată. Odată ce acest lucru este terminat, trimite cu bucurie rezultatul înapoi în bucla de evenimente, completând astfel ciclul!

Tutorial ClojureScript: Rezultatul final

Asa de! Ne-am definit abstractizarea stocării. Cum arată componenta acum?

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

Și componenta noastră a aplicației:

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

Și, în sfârșit, accesarea jetonului nostru într-o componentă la distanță este la fel de simplă ca:

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

Punând totul împreună:

Cum funcționează statul local și statul global (Redux) în exemplul de conectare.

Fără tam-tam, fără zgomot.

Decuplarea componentelor cu Redux/Re-frame înseamnă management curat al stării

Folosind Redux (prin re-frame), am decuplat cu succes componentele noastre de vizualizare de mizeria procesării stării. Extinderea abstracției noastre de stat este acum o bucată de tort!

Redux în ClojureScript este într-adevăr atât de ușor - nu aveți nicio scuză să nu încercați.

Dacă sunteți gata să faceți crack, aș recomanda să consultați documentele fantastice de reîncadrare și exemplul nostru simplu. Aștept cu nerăbdare să citesc comentariile dumneavoastră la acest tutorial ClojureScript de mai jos. Mult noroc!

Înrudit: Managementul stării în Angular folosind Firebase