~ / track C / clojure ecosystem
ClojureScript
IntermediateClojureScript is Clojure that compiles to JavaScript. It runs in browsers, Node, Deno, React Native, and JS-embedding hosts. The semantics are deliberately close to JVM Clojure — persistent collections, macros, the whole sequence library — but it's a different runtime with different idioms for I/O, interop, and tooling.
What's the same
- Persistent vectors, maps, sets, lists — all present.
defn,let,loop/recur, destructuring, threading macros — all present.- Macros — defined in
.cljfiles, used from.cljs(because macros run at compile time on the JVM). core.async, transducers, spec, malli — same APIs.
What's different
| Clojure (JVM) | ClojureScript | |
|---|---|---|
| Numbers | Long, Double, BigInt | All numbers are JS Number (double) |
| Strings | java.lang.String | JS String |
= on numbers | Strict by type | Same (= 1 1.0) is false |
| Threads | future, pmap, real OS threads | Single-threaded JS event loop; use core.async or Promise for async |
| Atom semantics | Same swap! semantics | Same — but no real concurrency |
| Interop syntax | (.method obj args) | Same; plus (.-property obj) for property access |
| Hashing of strings | Java's hashCode | Reimplemented to match Clojure |
| Reflection | Yes (and you can hint it away) | No — direct property access compiled inline |
| AOT compilation | Optional | Mandatory — the compiler emits JS |
Hello CLJS
;; src/myapp/core.cljs
(ns myapp.core)
(defn ^:export greet [name]
(str "Hello, " name "!"))
(js/console.log (greet "Clojure"))
^:export keeps the symbol from being renamed by the Google Closure
advanced compiler.
JS interop
;; Property: .-key (note the leading dash)
(.-name js/document) ;; document.name
(set! (.-title js/document) "x") ;; document.title = "x"
;; Method
(.querySelector js/document "#root")
;; => same as: document.querySelector("#root")
;; Object literal — use js-obj or #js
(js-obj "a" 1 "b" 2)
#js {:a 1 :b 2} ;; reader literal; values must be JS-compatible
;; Array
(js/Array. 1 2 3)
#js [1 2 3]
;; Convert between JS and CLJS
(js->clj #js {:a 1} :keywordize-keys true) ;; => {:a 1}
(clj->js {:a 1}) ;; => #js {:a 1}
The build chain
Three tools dominate; shadow-cljs is the default for application work:
| Tool | What it is |
|---|---|
ClojureScript compiler (cljs.main) | The official tool; integrates with deps.edn |
| shadow-cljs | Wraps the compiler with great npm interop, hot reload, multiple builds |
| Figwheel Main | Older live-reload setup; still works; less used than shadow |
# shadow-cljs
npx shadow-cljs watch app # dev, with hot reload
npx shadow-cljs release app # production, advanced compilation
shadow-cljs.edn configures builds (targets, main namespaces, deps,
which Closure compiler optimization to use).
React via Reagent
(ns myapp.ui
(:require [reagent.core :as r]
[reagent.dom :as rdom]))
(defonce state (r/atom {:n 0}))
(defn counter []
[:div
[:p "Count: " (:n @state)]
[:button {:on-click #(swap! state update :n inc)} "+"]])
(rdom/render [counter] (.getElementById js/document "root"))
- Components are functions returning vectors — Hiccup syntax.
r/atomis a reactive atom; deref'ing it inside a component subscribes that component to changes.- No JSX, no class components, no useEffect/useState — just functions and atoms.
Re-frame sits on top with a global app-db and dispatch/subscribe
flow. UIx and Helix are React-hooks-first alternatives that interop
with modern React patterns more directly.
Hot reload
Both shadow and Figwheel can re-evaluate changed namespaces without a
page refresh. Component state in defonce survives reloads, so you can
mutate UI code and keep your current app state visible. This is the
ClojureScript equivalent of the JVM [[repl-driven-development]] flow.
Try it
ClojureScript can be loaded into the browser via a self-hosted compiler
(klipse / SCI). The in-page Repl already uses one:
Performance notes
- Persistent data structures are noticeably slower than native JS
arrays/objects for hot inner loops. Use
arrayandagetin those. - Advanced compilation renames everything; external symbols you
expose to JS need
^:exportor an externs file. - ClojureScript starts up slower than vanilla JS — load size and JIT warmup. The runtime is fine; the bundle is the bottleneck.
Real-world
| Library | Use |
|---|---|
reagent | Reactive React wrapper — most common UI base |
re-frame | Flux-style architecture on Reagent |
uix / helix | Modern React-hooks-style alternatives |
shadow-cljs | The build tool (npm, hot reload, multiple targets) |
figwheel.main | Older hot-reload tool, still functional |
cljs-ajax / cljs-http | HTTP clients |
cljs.spec.alpha | Same spec API as JVM ([[clojure-spec]]) |
| Node target | shadow's :target :node builds CLIs and servers |
| React Native | shadow-cljs :target :react-native |
| Krell | Lightweight RN/native target |
Check yourself
? quiz
In ClojureScript, what does `(.-length s)` do?
Exercise
Build a tiny counter app in Reagent:
- A
defonce app-stateholding{:n 0}. - Two components:
display(shows:n) andcontrols(+/-buttons thatswap!the atom). - Render both inside a root component.
Now factor the buttons into a single component that takes a :delta
prop. What happens to re-render granularity when only one button is
clicked — does the whole tree re-render or just the changed parts?
(Hint: Reagent's tracking via @app-state is fine-grained.)