Fase 3: Introdução ao Frontend (Reagent e Shadow-CLJS)¶
Objetivo: Aprender os fundamentos do ClojureScript e do Reagent. Vamos construir uma UI interativa que gerencia seu próprio estado (na memória do navegador), sem se comunicar com a API ainda.
Tip
Nesta fase, o servidor de backend (porta 3000) não é necessário. Pode deixá-lo desligado. Vamos trabalhar apenas com o frontend (porta 8000).
Passo 3.1: Setup do Ambiente Node.js (npm)¶
O shadow-cljs (nosso compilador de frontend) é uma ferramenta "híbrida": ele é uma biblioteca Clojure (que entrará no deps.edn), mas também é uma ferramenta de linha de comando que roda em Node.js. Além disso, o React será instalado via npm.
Ação: Na raiz do projeto (todo-app/), execute estes dois comandos:
# 1. Cria o package.json (o "deps.edn" do mundo Node.js)
npm init -y
# 2. Instala as ferramentas, com versões FIXADAS
npm install shadow-cljs@2.28.23 react@19.2.0 react-dom@19.2.0
O que esses comandos fazem?
npm init -ycria o arquivopackage.json, que rastreia as dependências de JavaScript do projeto (o-yaceita todas as respostas padrão).npm install ...baixa as ferramentas para a pastanode_modules/(que nosso.gitignoreda Fase 0 já está, corretamente, ignorando) e as registra nopackage.json.
Warning
Por que fixamos as versões? O shadow-cljs existe em dois lugares: como pacote npm (a linha de comando) e como biblioteca Maven no deps.edn (o compilador em si). As duas versões precisam ser idênticas. Se você rodar apenas npm install shadow-cljs (sem versão), o npm instalará a versão mais recente (3.x), que não vai bater com a que colocaremos no deps.edn — e você verá avisos de "version mismatch" e erros estranhos de compilação. Fixar 2.28.23 nos dois lugares elimina esse problema pela raiz.
Passo 3.2: Configurar as Bibliotecas do Frontend (deps.edn)¶
O que é o Reagent? O Reagent é uma biblioteca que nos permite usar o React (a popular biblioteca de UI) de uma forma muito limpa e "Clojure-nativa". Escrevemos vetores simples (chamados Hiccup), e o Reagent os transforma em componentes React super rápidos.
Ação: Abra o deps.edn e adicione as duas linhas do frontend ao bloco :deps:
{:paths ["src" "resources"]
:deps
{org.clojure/clojure {:mvn/version "1.11.1"}
;; --- Dependências do Backend (API REST) ---
ring/ring-core {:mvn/version "1.12.2"}
ring/ring-jetty-adapter {:mvn/version "1.12.2"}
ring/ring-json {:mvn/version "0.5.1"}
metosin/reitit-ring {:mvn/version "0.7.0"}
;; --- Dependências do Frontend ---
;; ADICIONE ESTAS DUAS LINHAS:
;; (a versão do shadow-cljs é a MESMA que instalamos via npm!)
thheller/shadow-cljs {:mvn/version "2.28.23"}
reagent/reagent {:mvn/version "2.0.0"}}
:aliases
{:run
{:main-opts ["-m" "todo.backend.core"]}}}
O que fizemos?¶
Nossa "lista de compras" Clojure agora inclui:
thheller/shadow-cljs2.28.23: o compilador — na mesma versão do pacote npm.reagent/reagent2.0.0: a biblioteca de UI, na versão moderna que funciona com React 18/19.
Passo 3.3: Configurar o Compilador (shadow-cljs.edn)¶
Temos as bibliotecas, mas precisamos de um arquivo para dizer ao shadow-cljs como compilar nosso frontend. Este arquivo é o "cérebro" do shadow-cljs.
Ação: Crie um arquivo novo chamado shadow-cljs.edn na raiz do projeto (ao lado do deps.edn) e cole:
{:deps true ;; 1. Informa ao shadow-cljs para usar o deps.edn
:builds
{:app ;; Damos ao nosso "build" (processo) o nome de ":app"
{;; :browser = o destino do nosso código é um navegador web
:target :browser
;; Onde o JavaScript compilado deve ser salvo
:output-dir "resources/public/js"
;; O caminho que o navegador usará para acessar esses arquivos
:asset-path "/js"
;; O "ponto de entrada" do nosso código.
;; Diz ao shadow-cljs para compilar o namespace 'todo.frontend.core'
;; e, quando a página carregar, chamar a função 'init'.
:modules {:main {:init-fn todo.frontend.core/init}}
;; Configurações do servidor de desenvolvimento
:devtools
{;; O servidor serve os arquivos estáticos (como o index.html)
;; desta pasta:
:http-root "resources/public"
;; O servidor roda na porta 8000 (diferente da API, na 3000)
:http-port 8000}}}}
O que fizemos?¶
:deps true: o shadow-cljs vai usar as dependências do nossodeps.edn(por isso as versões precisam bater!).:output-dir: o JavaScript final irá pararesources/public/js(pasta que o.gitignorejá ignora).:modules: o "ponto de partida" é a funçãoinitno namespacetodo.frontend.core(que ainda não existe — criaremos já já).:devtools: o servidor de desenvolvimento roda na porta 8000 e serve os arquivos deresources/public.
Passo 3.4: Criar o HTML Base¶
O shadow-cljs precisa de um index.html para servir ao navegador. Este arquivo é a "concha" onde nossa aplicação ClojureScript será carregada.
Ação 1: Crie a pasta que o servidor de desenvolvimento usa:
Ação 2: Crie o arquivo resources/public/index.html e cole:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Todo App (CLJS)</title>
</head>
<body>
<div id="app">Carregando...</div>
<script src="/js/main.js"></script>
</body>
</html>
O que fizemos?¶
<div id="app">: o "ponto de montagem" crucial. Quando nossa aplicação iniciar, ela vai procurar estedive injetar toda a interface dentro dele.<script src="/js/main.js">: carrega o JavaScript compilado que o shadow-cljs vai gerar (o nomemain.jsvem do módulo:maindoshadow-cljs.edn).
Passo 3.5: "Olá, Reagent!" (UI Estática)¶
Objetivo: provar que o compilador consegue ler nosso arquivo .cljs, transformá-lo em JavaScript e injetá-lo no index.html. Sem estado, sem botões — apenas um "Olá".
Ação 1: Crie a pasta do frontend:
Ação 2: Crie o arquivo src/todo/frontend/core.cljs e cole:
(ns todo.frontend.core
;; Requeremos o "coração" do Reagent (para 'r/atom' e 'r/as-element')
(:require [reagent.core :as r]
;; E o DOM do Reagent (para 'create-root', API do React 18+)
[reagent.dom.client :as rdom]))
;; --- 1. O Componente ---
;; Um componente em Reagent é apenas uma função
;; que retorna "Hiccup" (HTML escrito como vetores CLJS).
;;
;; [:h1 "Olá"] -> <h1>Olá</h1>
(defn hello-world []
[:div
[:h1 "Olá, Alunos!"]
[:p "Nossa aplicação ClojureScript está funcionando."]])
;; --- 2. A Inicialização (React 18+) ---
;; Esta é a função que o shadow-cljs.edn chama.
;; Ela "monta" nosso componente [hello-world]
;; no <div id="app"> do nosso index.html.
(defn ^:export init []
(println "Frontend inicializado...")
(let [root (rdom/create-root (js/document.getElementById "app"))]
(.render root (r/as-element [hello-world]))))
Ação 3: Compile e rode
No terminal (pode ser o mesmo de antes, já que o backend está desligado):
- Na primeira vez, o shadow-cljs vai baixar várias dependências — pode demorar alguns minutos. Espere até ver algo como
Build completed. - O comando
watchfica observando seus arquivos: cada vez que você salvar um.cljs, ele recompila automaticamente (hot-reload). Deixe este terminal rodando — ele será o nosso Terminal 2 (Frontend) daqui em diante.
Ação 4: Abra o navegador em http://localhost:8000
Resultado Esperado: a página não deve mais mostrar "Carregando...". Em vez disso:
Olá, Alunos! Nossa aplicação ClojureScript está funcionando.
Se algo deu errado…¶
| Sintoma | Causa provável |
|---|---|
shadow-cljs - dependency version mismatch |
A versão do shadow-cljs no npm é diferente da do deps.edn. Confira o Passo 3.1/3.2: ambas devem ser 2.28.23. |
| Página continua em "Carregando..." | O build falhou. Olhe o terminal do watch: haverá um erro de compilação apontando arquivo e linha. Console do navegador (F12) também ajuda. |
port 8000 already in use |
Outro processo está usando a porta. Pare-o ou mude o :http-port no shadow-cljs.edn. |
Cannot find module 'react' |
O npm install do Passo 3.1 não foi executado (ou foi executado em outra pasta). |
Passo 3.6: O "Cérebro" Reativo (o r/atom)¶
A ligação: atom (Backend) vs. r/atom (Frontend)
Na Fase 2, aprendemos o atom do Clojure:
- O problema: a imutabilidade — não podíamos "mudar" um mapa.
- A solução: uma "caixa" segura onde trocamos (
swap!) o valor antigo por um novo.
No frontend, temos um problema parecido: como a UI (o HTML) vai "saber" quando um valor muda? A solução do Reagent é a sua própria versão do atom: reagent.core/atom (que apelidamos de r/atom).
Qual é a diferença?
- Sintaxe: nenhuma! Você usa
@para ler,swap!para atualizar ereset!para substituir — exatamente como aprendeu. - A "mágica": o
r/atomé uma caixa que "toca um sino". Qualquer componente que "lê" umr/atom(usando@) está "ouvindo" esse sino. Quando você usaswap!oureset!, o sino toca, e o Reagent redesenha automaticamente apenas os componentes que estavam ouvindo.
Vamos provar isso com um contador.
Ação 1: Substitua todo o conteúdo do src/todo/frontend/core.cljs por:
(ns todo.frontend.core
(:require [reagent.core :as r]
[reagent.dom.client :as rdom]))
;; --- 1. O "Cérebro" (o r/atom) ---
;;
;; (defonce) garante que o valor (o 0) NÃO seja resetado
;; toda vez que o "hot-reload" do shadow-cljs recompilar.
(defonce app-state (r/atom 0))
;; --- 2. O Componente ---
(defn counter-app []
[:div
[:h1 "Entendendo o 'r/atom'"]
;; (Leitura): "lemos" o valor dentro da caixa usando '@'
[:p "O valor atual do contador é: " @app-state]
[:button
;; (Escrita): no clique, usamos 'swap!' para "trocar"
;; o valor na caixa, aplicando a função 'inc'.
{:on-click #(swap! app-state inc)}
"Clique para Incrementar"]
[:button
{:on-click #(reset! app-state 0)}
"Resetar"]])
;; --- 3. A Inicialização ---
(defn ^:export init []
(println "Frontend 'Contador' inicializado...")
(let [root (rdom/create-root (js/document.getElementById "app"))]
(.render root (r/as-element [counter-app]))))
Ação 2 (Teste):
- Salve o arquivo. O shadow-cljs (Terminal 2) recompila sozinho.
- Vá ao navegador em
http://localhost:8000. - Teste a mágica: clique nos botões. O número muda instantaneamente, sem recarregar a página.
A lição: você acabou de aprender o "coração" do Reagent: (defonce app-state (r/atom ...)) (o cérebro), @app-state (leitura) e swap!/reset! (escrita).
Passo 3.7: A UI Estática do Todo (com dados falsos)¶
Objetivo: agora que sabemos como o Reagent funciona, vamos construir o visual (estático) do Todo App. Ele não vai funcionar ainda — apenas parecer correto.
Ação: Substitua o conteúdo do core.cljs (o contador) por este, agora separado em componentes:
(ns todo.frontend.core
(:require [reagent.core :as r]
[reagent.dom.client :as rdom]))
;; --- 1. Nossos Componentes (Estáticos) ---
;; O Formulário (não faz nada ainda)
(defn todo-form []
[:div.todo-input
[:input {:type "text" :placeholder "O que precisa ser feito?"}]
[:button "Adicionar (Desligado)"]])
;; A Lista (recebe uma lista de "todos" como argumento)
(defn todo-list [todos] ;; "todos" é um argumento!
[:ul.todo-list
(for [todo todos]
^{:key (:id todo)}
[:li.todo-item
(:title todo)])])
;; O App Principal (que monta tudo)
(defn app []
[:div.todo-app
[:h1 "Todo App (Estático)"]
[:p "Isto é apenas o visual. Nada funciona ainda."]
;; 1. Renderiza o formulário
[todo-form]
;; 2. Renderiza a lista, passando "dados falsos"
[todo-list [{:id 1 :title "Meu primeiro item (falso)"}
{:id 2 :title "Meu segundo item (falso)"}]]])
;; --- 2. A Inicialização ---
(defn ^:export init []
(println "Frontend 'Todo Estático' inicializado...")
(let [root (rdom/create-root (js/document.getElementById "app"))]
(.render root (r/as-element [app]))))
Tip
O que é o ^{:key ...}? Quando renderizamos uma lista com for, o React precisa de uma "identidade" para cada item, para saber o que mudou entre uma renderização e outra. O metadado ^{:key (:id todo)} fornece essa identidade. Sem ele, o console mostra o aviso "Every element in a seq should have a unique :key". Guarde esse detalhe: ele vai reaparecer na Fase 5.
Teste: salve e veja no navegador. Você deve ver a UI completa, com os dois todos falsos listados. O campo e o botão estão lá, mas clicar não faz nada.
Passo 3.8: Conectando a UI ao r/atom (Tornando Interativo)¶
Agora vamos fazer o formulário funcionar de verdade — mas ainda 100% local, sem API.
Ação 1: Adicionar o clojure.string
No ns do core.cljs, precisamos do clojure.string para checar se o input está em branco. Mude isto:
Para isto:
(ns todo.frontend.core
(:require [reagent.core :as r]
[reagent.dom.client :as rdom]
[clojure.string :as str])) ;; <- ADICIONE ESTA LINHA
Ação 2: Adicionar o "Cérebro" (app-state) e a lógica local
Logo abaixo do ns, adicione este bloco. Este é o r/atom central e a função local que o modifica:
;; --- O "Cérebro" Reativo ---
(defonce app-state (r/atom {:next-id 1
:input-text ""
:todos []}))
;; --- A Lógica de Negócios (Local) ---
(defn adicionar-todo-local []
(swap! app-state
(fn [estado-atual]
(let [novo-titulo (:input-text estado-atual)
novo-id (:next-id estado-atual)]
(if (str/blank? novo-titulo)
estado-atual ;; Não faz nada se vazio
;; Retorna um NOVO estado
{:next-id (inc novo-id)
:input-text "" ;; Limpa o input
:todos (conj (:todos estado-atual)
{:id novo-id
:title novo-titulo})})))))
Ação 3: Modificar o todo-form
Encontre a função todo-form e conecte-a ao app-state. Mude esta versão (estática):
(defn todo-form []
[:div.todo-input
[:input {:type "text" :placeholder "O que precisa ser feito?"}]
[:button "Adicionar (Desligado)"]])
Para esta versão (conectada):
(defn todo-form []
[:div.todo-input
[:input
{:type "text"
:placeholder "O que precisa ser feito?"
;; (Leitura): o valor do input vem do app-state
:value (:input-text @app-state)
;; (Escrita): o on-change atualiza o app-state a cada tecla
:on-change #(swap! app-state assoc :input-text (-> % .-target .-value))}]
[:button
;; (Ação): o on-click chama nossa lógica local
{:on-click adicionar-todo-local}
"Adicionar (Local)"]])
Ação 4: Modificar o todo-list
Vamos remover o argumento [todos] e fazê-la ler diretamente do app-state. Mude esta versão:
(defn todo-list [todos] ;; <-- Recebe "todos" como argumento
[:ul.todo-list
(for [todo todos]
^{:key (:id todo)}
[:li.todo-item
(:title todo)])])
Para esta versão:
(defn todo-list [] ;; <-- Argumento "todos" REMOVIDO
[:ul.todo-list
;; (Leitura): o 'for' agora observa o @app-state
(for [todo (:todos @app-state)]
^{:key (:id todo)}
[:li.todo-item
(:title todo)])])
Ação 5: Modificar o app (remover os dados falsos)
Mude esta versão:
(defn app []
[:div.todo-app
[:h1 "Todo App (Estático)"]
[:p "Isto é apenas o visual. Nada funciona ainda."]
[todo-form]
[todo-list [{:id 1 :title "Meu primeiro item (falso)"}
{:id 2 :title "Meu segundo item (falso)"}]]])
Para esta versão (limpa e interativa):
(defn app []
[:div.todo-app
[:h1 "Todo App (Somente Frontend)"]
[:p "Isto é 100% local. Recarregue (F5) para ver os dados sumirem."]
;; Os componentes agora se viram sozinhos!
[todo-form]
[todo-list]])
Teste (A "Lição")¶
- Salve o
core.cljs. O shadow-cljs recompila. - Vá ao navegador em
http://localhost:8000.
Teste a interatividade:
- A lista deve estar vazia.
- Digite "Meu primeiro todo" e clique em "Adicionar (Local)".
- Mágica: o todo aparece na lista instantaneamente, e o input é limpo.
O "Aha!": agora, recarregue a página (F5).
Todos os todos que você adicionou desapareceram.
Isso prova que o estado vivia apenas na memória do navegador. Agora temos uma motivação clara para a próxima fase: como fazer essa lista persistir? A resposta é a nossa API do backend.
Passo 3.9: Git Checkpoint¶
Por que fazemos isso?
Acabamos de construir uma "mini-aplicação" de frontend completa: npm, shadow-cljs, react e a reatividade do Reagent. É o ponto de salvamento perfeito antes de misturar a complexidade da API.
Ação 1: Pare o shadow-cljs (Ctrl+C no Terminal 2) e verifique:
Você deve ver os novos arquivos e modificações: package.json, package-lock.json, shadow-cljs.edn, resources/public/index.html, src/todo/frontend/core.cljs e o deps.edn modificado. (Repare que node_modules/ e resources/public/js/ não aparecem — obrigado, .gitignore!)
Ação 2: Prepare e salve — este é um único commit, com exatamente esta mensagem (ela é um dos marcos avaliados):
Fim da Fase 3! 🏁
Temos um backend funcional (Fase 2) e um frontend funcional (Fase 3), em commits separados. Eles ainda não se "conhecem".