Controle de nível superior com gerenciamento de estado do Redux: um tutorial do ClojureScript
Publicados: 2022-03-11Bem-vindo de volta para a segunda edição emocionante do Unearthing ClojureScript! Neste post, abordarei o próximo grande passo para levar a sério o ClojureScript: gerenciamento de estado—neste caso, usando React.
Com o software front-end, o gerenciamento de estado é um grande negócio. Fora da caixa, existem algumas maneiras de lidar com o estado no React:
- Mantendo o estado no nível superior e passando-o (ou manipuladores para uma parte específica do estado) para os componentes filhos.
- Jogando a pureza pela janela e tendo variáveis globais ou alguma forma Lovecraftiana de injeção de dependência.
De um modo geral, nenhum deles é ótimo. Manter o estado no nível superior é bastante simples, mas há uma grande sobrecarga para passar o estado do aplicativo para todos os componentes que precisam dele.
Em comparação, ter variáveis globais (ou outras versões ingênuas de estado) pode resultar em problemas de simultaneidade difíceis de rastrear, fazendo com que os componentes não sejam atualizados quando você espera, ou vice-versa.
Então, como isso pode ser enfrentado? Para aqueles que estão familiarizados com o React, você pode ter experimentado o Redux, um contêiner de estado para aplicativos JavaScript. Você pode ter descoberto isso por sua própria vontade, procurando corajosamente por um sistema gerenciável para manter o estado. Ou você pode ter tropeçado nele enquanto lia sobre JavaScript e outras ferramentas da web.
Independentemente de como as pessoas acabem olhando para o Redux, na minha experiência elas geralmente terminam com dois pensamentos:
- “Eu sinto que tenho que usar isso porque todo mundo diz que eu tenho que usar.”
- “Eu realmente não entendo completamente por que isso é melhor.”
De um modo geral, o Redux fornece uma abstração que permite que o gerenciamento de estado se encaixe na natureza reativa do React. Ao descarregar todo o statefulness para um sistema como o Redux, você preserva a pureza do React. Assim, você acabará com muito menos dores de cabeça e, geralmente, algo muito mais fácil de raciocinar.
Para quem é novo no Clojure
Embora isso possa não ajudá-lo a aprender o ClojureScript inteiramente do zero, aqui vou pelo menos recapitular alguns conceitos básicos de estado no Clojure[Script]. Sinta-se à vontade para pular essas partes se você já for um Clojurian experiente!
Lembre-se de um dos fundamentos do Clojure que também se aplica ao ClojureScript: Por padrão, os dados são imutáveis. Isso é ótimo para desenvolver e ter garantias de que o que você cria no timestep N ainda é o mesmo no timestep > N. ClojureScript também nos fornece uma maneira conveniente de ter um estado mutável se precisarmos, através do conceito de atom .
Um atom em ClojureScript é muito semelhante a um AtomicReference em Java: ele fornece um novo objeto que bloqueia seu conteúdo com garantias de simultaneidade. Assim como em Java, você pode colocar o que quiser neste objeto - a partir de então, esse átomo será uma referência atômica para o que você quiser.
Depois de ter seu atom , você pode definir atomicamente um novo valor usando o reset! função (observe o ! na função — na linguagem Clojure isso é frequentemente usado para significar que uma operação é stateful ou impura).
Observe também que, ao contrário do Java, o Clojure não se importa com o que você coloca em seu atom . Pode ser uma string, uma lista ou um objeto. Digitação dinâmica, querida!
(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 :) Reagente estende este conceito de átomo com seu próprio atom . (Se você não estiver familiarizado com o Reagent, confira o post anterior.) Isso se comporta de forma idêntica ao ClojureScript atom , exceto que também aciona eventos de renderização no Reagent, assim como o armazenamento de estado interno do React.
Um exemplo:
(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!")}]]) Isso mostrará um único <div> contendo um <span> dizendo “Hello, world!” e um botão, como você poderia esperar. Pressionar esse botão transformará atomicamente my-atom para conter "there!" . Isso acionará um redesenho do componente, resultando no span dizendo “Hello, there!” em vez de.
Isso parece simples o suficiente para uma mutação local em nível de componente, mas e se tivermos um aplicativo mais complicado que tenha vários níveis de abstração? Ou se precisarmos compartilhar um estado comum entre vários subcomponentes e seus subcomponentes?
Um exemplo mais complicado
Vamos explorar isso com um exemplo. Aqui estaremos implementando uma página de login bruta:
(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)}]]) Em seguida, hospedaremos esse componente de login em nosso app.cljs principal, assim:
(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]])O fluxo de trabalho esperado é assim:
- Aguardamos o usuário digitar seu nome de usuário e senha e clicar em enviar.
- Isso acionará nossa função
do-login-iono componente pai. - A função
do-login-iorealiza alguma operação de E/S (como efetuar login em um servidor e recuperar um token).
Se esta operação está bloqueando, então já estamos com um monte de problemas, pois nosso aplicativo está congelado - se não estiver, então temos que nos preocupar com assíncrona!
Além disso, agora precisamos fornecer esse token para todos os nossos subcomponentes que desejam fazer consultas ao nosso servidor. A refatoração de código ficou muito mais difícil!
Finalmente, nosso componente agora não é mais puramente reativo — agora é cúmplice no gerenciamento do estado do resto do aplicativo, acionando E/S e geralmente sendo um pouco incômodo.
Tutorial ClojureScript: Digite Redux
Redux é a varinha mágica que torna realidade todos os seus sonhos baseados no estado. Devidamente implementado, ele fornece uma abstração de compartilhamento de estado que é segura, rápida e fácil de usar.
O funcionamento interno do Redux (e a teoria por trás dele) está um pouco fora do escopo deste artigo. Em vez disso, vou mergulhar em um exemplo de trabalho com ClojureScript, que deve servir para demonstrar do que é capaz!
Em nosso contexto, o Redux é implementado por uma das muitas bibliotecas ClojureScript disponíveis; este chamado re-frame. Ele fornece um wrapper do Clojure em torno do Redux que (na minha opinião) o torna um prazer absoluto de usar.
O básico
O Redux eleva o estado do seu aplicativo, deixando seus componentes leves. Um componente Reduxificado só precisa pensar em:
- O que isso parece
- Quais dados ele consome
- Quais eventos ele desencadeia
O resto é tratado nos bastidores.
Para enfatizar este ponto, vamos Reduxificar nossa página de login acima.
O banco de dados
Primeiras coisas primeiro: Precisamos decidir como será o nosso modelo de aplicativo. Fazemos isso definindo a forma de nossos dados, dados que estarão acessíveis em todo o aplicativo.

Uma boa regra geral é que, se os dados precisarem ser usados em vários componentes do Redux ou precisarem durar muito (como nosso token), eles deverão ser armazenados no banco de dados. Por outro lado, se os dados são locais para o componente (como nossos campos de nome de usuário e senha), eles devem viver como estado do componente local e não serem armazenados no banco de dados.
Vamos criar nosso clichê de banco de dados e especificar nosso 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})Há alguns pontos interessantes que merecem destaque aqui:
- Usamos a biblioteca de
specdo Clojure para descrever a aparência dos nossos dados. Isso é especialmente apropriado em uma linguagem dinâmica como Clojure[Script]. - Para este exemplo, estamos acompanhando apenas um token global que representará nosso usuário depois que ele fizer login. Esse token é uma string simples.
- No entanto, antes que o usuário faça login, não teremos um token. Isso é representado pela palavra-chave
:opt-un, que significa “opcional, não qualificado”. (No Clojure, uma palavra-chave regular seria algo como:cat, enquanto uma palavra-chave qualificada poderia ser algo como:animal/cat. A qualificação normalmente ocorre no nível do módulo - isso impede que as palavras-chave em diferentes módulos se sobreponham.) - Por fim, especificamos o estado padrão do nosso banco de dados, que é como ele é inicializado.
A qualquer momento, devemos ter certeza de que os dados em nosso banco de dados correspondem às nossas especificações aqui.
Assinaturas
Agora que descrevemos nosso modelo de dados, precisamos refletir como nossa visão mostra esses dados. Já descrevemos como nossa visão se parece em nosso componente Redux - agora simplesmente precisamos conectar nossa visão ao nosso banco de dados.
Com o Redux, não acessamos nosso banco de dados diretamente - isso pode resultar em problemas de ciclo de vida e simultaneidade. Em vez disso, registramos nosso relacionamento com uma faceta do banco de dados por meio de assinaturas .
Uma assinatura informa ao re-frame (e ao Reagent) que dependemos de uma parte do banco de dados e, se essa parte for alterada, nosso componente Redux deverá ser renderizado novamente.
As assinaturas são muito simples 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)Aqui, registramos uma única assinatura – para o próprio token. Uma assinatura é simplesmente o nome da assinatura e a função que extrai esse item do banco de dados. Podemos fazer o que quisermos com esse valor e alterar a visão o quanto quisermos aqui; entretanto, neste caso, estamos simplesmente extraindo o token do banco de dados e retornando-o.
Há muito, muito mais que você pode fazer com assinaturas—como definir visualizações em subseções do banco de dados para um escopo mais restrito na re-renderização—mas vamos mantê-lo simples por enquanto!
Eventos
Temos nosso banco de dados e nossa visão do banco de dados. Agora precisamos acionar alguns eventos! Neste exemplo, temos dois tipos de eventos:
- O evento puro ( sem efeito colateral) de gravar um novo token no banco de dados.
- O evento de E/S ( tendo um efeito colateral) de sair e solicitar nosso token por meio de alguma interação com o cliente.
Vamos começar com o mais fácil. Re-frame ainda fornece uma função exatamente para esse 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))) Novamente, é bem direto aqui – definimos dois eventos. A primeira é para inicializar nosso banco de dados. (Vê como ele ignora ambos os argumentos? Nós sempre inicializamos o banco de dados com nosso default-db !) O segundo é para armazenar nosso token assim que o obtivermos.
Observe que nenhum desses eventos tem efeitos colaterais - nenhuma chamada externa, nenhuma E/S! Isso é muito importante para preservar a santidade do sagrado processo Redux. Não o torne impuro para não desejar a ira do Redux sobre você.
Finalmente, precisamos do nosso evento de login. Vamos colocá-lo sob os outros:
(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])))) A função reg-event-fx é bastante semelhante a reg-event-db , embora haja algumas diferenças sutis.
- O primeiro argumento não é mais apenas o próprio banco de dados. Ele contém uma infinidade de outras coisas que você pode usar para gerenciar o estado do aplicativo.
- O segundo argumento é muito parecido com
reg-event-db. - Em vez de apenas retornar o novo
db, retornamos um mapa que representa todos os efeitos (“fx”) que devem ocorrer para este evento. Neste caso, simplesmente chamamos o efeito:request-token, que é definido abaixo. Um dos outros efeitos válidos é:dispatch, que simplesmente chama outro evento.
Uma vez que nosso efeito tenha sido despachado, nosso efeito :request-token é chamado, que executa nossa operação de login de E/S de longa duração. Uma vez terminado, ele despacha o resultado de volta para o loop de eventos, completando assim o ciclo!
Tutorial ClojureScript: O resultado final
Assim! Definimos nossa abstração de armazenamento. Como está o componente agora?
(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]})}]])E nosso componente de aplicativo:
(ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- VIEW -- (defn component [] [:div [login/component]])E, finalmente, acessar nosso token em algum componente remoto é tão simples quanto:
(let [token @(rf/subscribe [:token])] ; ... )Juntando tudo:
Sem confusão, sem confusão.
Desacoplar componentes com redux/re-frame significa gerenciamento de estado limpo
Usando Redux (via re-frame), nós separamos com sucesso nossos componentes de visão da bagunça do manuseio de estado. Estender nossa abstração de estado agora é fácil!
Redux em ClojureScript é realmente fácil - você não tem desculpa para não tentar.
Se você está pronto para quebrar, eu recomendo verificar os fantásticos documentos de re-frame e nosso exemplo simples e trabalhado. Estou ansioso para ler seus comentários sobre este tutorial ClojureScript abaixo. Boa sorte!
