ClojureScript para Desenvolvedores React
Se você escreve React, já entende a maior parte da história de front-end do ClojureScript — só ainda não sabe disso. Reagent, a biblioteca de UI dominante em ClojureScript, é React por baixo. Componentes são funções que retornam markup, estado dispara re-renders, o virtual DOM faz diff e patch. O que muda é a linguagem ao redor: dados imutáveis por padrão, s-expressions no lugar de JSX, e uma ferramenta de build que você dirige de um REPL. Este é o mapa que eu daria ao meu eu React do passado.
Componentes Ainda São Só Funções Retornando Markup
Um function component do React retorna JSX. Um componente Reagent retorna hiccup — vetores ClojureScript puros descrevendo o DOM. Keyword para a tag, um map para props, depois filhos:
(defn greeting [name]
[:div.greeting ; <div class="greeting">
[:h1 "Hello, " name]
[:button {:on-click #(js/alert "hi")} "Wave"]])
Isso é <div className="greeting"><h1>Hello, {name}</h1><button onClick={...}>Wave</button></div>. Sem transform de JSX, sem DSL de template — o markup são estruturas de dados que sua linguagem já tem, o que significa que você manipula views com o mesmo map/filter/assoc que usa em qualquer outro dado:
(defn order-list [orders]
[:ul
(for [o orders]
^{:key (:id o)} [:li (:name o) " — $" (:total o)])]) ; map over data → children
O ^{:key ...} é a key de reconciliação do React, mesmo conceito do key={} em JSX.
Estado: ratoms no Lugar de useState
O useState do React te dá um valor e um setter que dispara um re-render. O equivalente do Reagent é um reactive atom (r/atom): um container mutável de um valor imutável. Faça deref (@) para ler; qualquer componente que faz deref dele re-renderiza quando ele muda.
(defn counter []
(let [n (r/atom 0)] ; created once (form-2 component)
(fn [] ; this render fn re-runs on change
[:div
[:span "Count: " @n] ; deref → this component tracks n
[:button {:on-click #(swap! n inc)} "+"]])))
O único detalhe para devs React: este é um componente "form-2" — o let externo roda uma vez (como o corpo de um setup de useState), e a função interna retornada é o que re-renderiza. Confunda isso e seu atom reseta a cada render. Passado isso, o modelo é exatamente o do React: estado local num ratom, estado global compartilhado em algo como re-frame, que comparei ao Redux aqui.
O Ganho Silencioso: Dados Imutáveis Tornam Re-renders Baratos
Aqui está o payoff que não é óbvio até você sentir. No React, evitar re-renders desnecessários significa memo, useMemo, useCallback e arrays de dependência cuidadosos — porque com objetos JS mutáveis, "esta prop de fato mudou?" não pode ser respondido por uma checagem barata de referência (dois objetos diferentes podem ser estruturalmente iguais, e um objeto mutado é referência-igual ao seu eu antigo).
Dados em ClojureScript são imutáveis e persistentes, então "isto mudou?" é uma checagem de igualdade de referência, e é sempre correta: se a referência é a mesma, o valor não mudou, garantido. O Reagent se apoia nisso para pular re-render de subárvores cujas entradas são idênticas, sem nenhuma da contabilidade manual de memoização. A coisa que você luta no React — renders desnecessários por churn de referência — em grande parte evapora porque igualdade é barata e honesta. (Esta é a mesma propriedade que faz as técnicas de renderização de datasets grandes no post sobre performance com re-frame funcionarem.)
Interop É Cidadão de Primeira Classe
Você não perde o ecossistema React. Você pode colocar qualquer componente React no hiccup com o adapter :>, passando props como um map:
(ns app.core
(:require ["react-select" :default Select] ; import an npm package
[reagent.core :as r]))
(defn country-picker [options]
[:> Select {:options options ; render a JS React component
:on-change #(js/console.log %)}])
js/console.log, (.-property obj) para acesso a campo, (.method obj args) para chamadas — interop com JavaScript é uma parte normal e cotidiana da linguagem, não uma saída de emergência. Você recorre a bibliotecas npm quando são a melhor ferramenta e escreve o resto em ClojureScript.
O Build: shadow-cljs
A ferramenta que torna isso agradável para desenvolvedores React é o shadow-cljs, porque ele trata npm como fonte de dependência de primeira classe — você npm install um pacote e require diretamente. Ele te dá hot reloading (edite um componente, veja atualizar com estado preservado) e um REPL conectado à app rodando no browser, então você avalia expressões ao vivo contra sua UI real. Vindo de um setup Webpack/Vite, a config é menor e a história do npm é mais suave do que você esperaria de um toolchain de "linguagem diferente".
Trade-offs
Concern React (JS/TS) ClojureScript (Reagent)
----------------- ------------------------ ----------------------------
Markup JSX (transform) hiccup (plain data)
Local state useState r/atom (ratom)
Avoiding renders memo/useMemo/useCallback ~free via reference equality
npm ecosystem native native via shadow-cljs + interop
Types TypeScript dynamic (spec/malli optional)
Hiring/onboarding huge pool small pool, ramp-up needed
A Lição
Para um desenvolvedor React, ClojureScript é menos um salto do que os parênteses sugerem: é o modelo mental do React que você já domina, expresso numa linguagem onde dados imutáveis são o padrão e não uma disciplina. Você troca a segurança estática do TypeScript e um enorme pool de contratação por correção de re-render genuinamente mais barata e um fluxo guiado pelo REPL. É uma troca real — um time nativo de JS não deveria trocar de leve — mas se você já lutou com uma parede de useMemo para domar re-renders, o lado ClojureScript dessa troca vai parecer alívio.