~ / track C / clojure ecosystem

ClojureScript

Intermediate

ClojureScript 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 .clj files, 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
NumbersLong, Double, BigIntAll numbers are JS Number (double)
Stringsjava.lang.StringJS String
= on numbersStrict by typeSame (= 1 1.0) is false
Threadsfuture, pmap, real OS threadsSingle-threaded JS event loop; use core.async or Promise for async
Atom semanticsSame swap! semanticsSame — but no real concurrency
Interop syntax(.method obj args)Same; plus (.-property obj) for property access
Hashing of stringsJava's hashCodeReimplemented to match Clojure
ReflectionYes (and you can hint it away)No — direct property access compiled inline
AOT compilationOptionalMandatory — 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:

ToolWhat it is
ClojureScript compiler (cljs.main)The official tool; integrates with deps.edn
shadow-cljsWraps the compiler with great npm interop, hot reload, multiple builds
Figwheel MainOlder 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/atom is 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:

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

Performance notes

  • Persistent data structures are noticeably slower than native JS arrays/objects for hot inner loops. Use array and aget in those.
  • Advanced compilation renames everything; external symbols you expose to JS need ^:export or 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

LibraryUse
reagentReactive React wrapper — most common UI base
re-frameFlux-style architecture on Reagent
uix / helixModern React-hooks-style alternatives
shadow-cljsThe build tool (npm, hot reload, multiple targets)
figwheel.mainOlder hot-reload tool, still functional
cljs-ajax / cljs-httpHTTP clients
cljs.spec.alphaSame spec API as JVM ([[clojure-spec]])
Node targetshadow's :target :node builds CLIs and servers
React Nativeshadow-cljs :target :react-native
KrellLightweight RN/native target

Check yourself

? quiz

In ClojureScript, what does `(.-length s)` do?

Exercise

Build a tiny counter app in Reagent:

  1. A defonce app-state holding {:n 0}.
  2. Two components: display (shows :n) and controls (+ / - buttons that swap! the atom).
  3. 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.)

 status: new