Skip to content

Clojure Guide — ClojureScript: Clojure for the Browser

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Clojure Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

ClojureScript is a compiler that translates Clojure to JavaScript, bringing Clojure's functional paradigms, immutable data structures, and concurrency model to browsers and JavaScript runtimes.

What You'll Learn

  • Setting up ClojureScript projects
  • ClojureScript vs Clojure differences
  • Interoperability with JavaScript
  • React integration with Reagent
  • Building and optimizing CLJS apps

Why It Matters

ClojureScript brings the same productivity gains as Clojure to frontend development: immutable data, REPL-driven development, and a concise syntax. Doda Browser uses ClojureScript for its extension API.

Real-World Use

CircleCI's frontend is built with ClojureScript. Netflix uses ClojureScript for internal tools. Nubank, a Brazilian fintech, built their entire app in ClojureScript with Reagent.

flowchart LR
    A["ClojureScript"] --> B["Setup"]
    B --> C["CLJS vs CLJ"]
    C --> D["JS Interop"]
    D --> E["Reagent"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Project Setup

;; deps.edn
{:deps {org.clojure/clojurescript {:mvn/version "1.11.60"}}}

;; project.clj
(defproject my-app "0.1.0"
  :dependencies [[org.clojure/clojurescript "1.11.60"]]
  :plugins [[lein-cljsbuild "1.1.8"]]
  :cljsbuild {:builds [{:id "dev"
                        :source-paths ["src"]
                        :compiler {:main "my-app.core"
                                   :output-to "public/js/main.js"
                                   :optimizations :none}}]})

ClojureScript vs Clojure

;; Same Clojure code works in both
(defn greet [name]
  (str "Hello, " name))

;; Different: CLJS uses js/* for host interop
;; CLJ: (System/currentTimeMillis)
;; CLJS: (.getTime (js/Date.))

;; Different: type hints
;; CLJ: ^String name
;; CLJS: ^js/String name or ^string name

JavaScript Interop

;; Access JavaScript properties
(.-length "hello")                    ; Property access
(.toUpperCase "hello")                ; Method call
(js/console.log "Hello from CLJS")   ; Global object

;; JavaScript object literals
#js {:name "Alice" :age 30}           ; JS object (not CLJS map)

;; Using JavaScript arrays
(aget #js [1 2 3] 0)                  ; Access index
(aset #js [1 2 3] 0 99)             ; Set index

;; Arrow functions (CLJS 1.10.600+)
(def fn-arrow #(js/console.log %))

DOM Manipulation

;; Direct DOM access
(js/document.getElementById "app")

;; Set innerHTML
(set! (.-innerHTML (js/document.getElementById "app"))
      "<h1>Hello from ClojureScript</h1>")

;; Event listeners
(.addEventListener (js/document.getElementById "btn")
                   "click"
                   #(js/console.log "Clicked!"))

Reagent (React Wrapper)

(require '[reagent.core :as r])

;; Simple component
(defn hello-component []
  [:div
   [:h1 "Hello from Reagent"]
   [:p "This is ClojureScript!"]])

;; Reactive state
(defn counter []
  (let [count (r/atom 0)]
    (fn []
      [:div
       [:p "Count: " @count]
       [:button {:on-click #(swap! count inc)} "Increment"]])))

;; Render
(r/render [counter] (js/document.getElementById "app"))

Working with npm

;; Shadow CLJS or figwheel.main for npm integration
:dependencies [[thheller/shadow-cljs "2.20.0"]]

;; shadow-cljs.edn
{:dependencies [[reagent "1.1.0"]]
 :builds {:app {:target :browser
                :output-dir "public/js"
                :assets-path "js"}}}

Reader Conditionals

;; Code that works in both CLJ and CLJS
(defn platform-info []
  #?(:clj  (str "Clojure " (clojure-version))
     :cljs (str "ClojureScript " (cljs-version))))

;; Reader conditional splicing
(def x
  #?@(:clj  [(System/currentTimeMillis)]
      :cljs [(.getTime (js/Date.))]))

Common Mistakes

1. Forgetting CLJS has no threading

ClojureScript runs in a single thread. Atoms work, but refs and agents are not available. Use core.async for concurrency.

2. Using JVM-only libraries

Not all Clojure libraries work in CLJS. Check for CLJS compatibility or use reader conditionals.

3. Confusing JS objects with CLJS data structures

#js {} creates JS objects. Clojure maps {} are persistent data structures. Use the right one for interop.

4. Ignoring advanced compilation warnings

:optimizations :advanced renames variables. Use ^:export and externs for public API.

5. Not using the REPL

CLJS has REPL support via Figwheel or shadow-cljs. Use it for interactive development.

Practice Questions

1. What is ClojureScript? A compiler that translates Clojure to JavaScript, enabling Clojure programming for browsers and JavaScript runtimes.

2. How does ClojureScript interop with JavaScript? Use js/ for globals, .- for property access, . for method calls, and #js for JS object literals.

3. What is Reagent? A minimal ClojureScript wrapper around React that renders Hiccup-style vectors as React components.

Challenge: Build a simple ClojureScript todo list using Reagent with local state management.

FAQ

{{< faq question="Is ClojureScript faster than JavaScript?" >}} ClojureScript performance is comparable to hand-written JavaScript when compiled with advanced optimizations. Immutable data structures add overhead but enable safer code. {{< /faq >}}

{{< faq question="Can I use npm packages with ClojureScript?" >}} Yes, through shadow-cljs or figwheel.main. These tools integrate with npm and provide module resolution. {{< /faq >}}

{{< faq question="What is the difference between CLJS and CLJ data structures?" >}} CLJS persistent data structures are implemented in JavaScript. They behave identically but have different performance characteristics than JVM Clojure. {{< /faq >}}

{{< faq question="Does ClojureScript support macros?" >}} Yes, but macros must be written in Clojure (JVM) and used in CLJS. The macroexpansion happens at build time on the JVM. {{< /faq >}}

{{< faq question="What build tools are available for CLJS?" >}} Leiningen with cljsbuild, shadow-cljs, figwheel.main, and deps.edn with CLI tools. Shadow-cljs is the most popular for modern projects. {{< /faq >}}

Mini Project

Create a simple interactive counter in ClojureScript with Reagent:

(ns my-app.core
  (:require [reagent.core :as r]))

(defn app []
  (let [count (r/atom 0)]
    (fn []
      [:div {:style {:text-align "center" :padding "2rem"}}
       [:h1 "ClojureScript Counter"]
       [:p {:style {:font-size "3rem"}} @count]
       [:button {:on-click #(swap! count inc)} "+"]
       [:button {:on-click #(swap! count dec)} "-"]
       [:button {:on-click #(reset! count 0)} "Reset"]])))

(defn ^:export init []
  (r/render [app] (js/document.getElementById "app")))

What's Next

Now that you understand ClojureScript, explore GraalVM for compiling Clojure to native binaries.

Topic Description Link
Clojure GraalVM Native compilation {{< ref "28-graalvm" >}}
Clojure Performance Optimization guide {{< ref "29-performance" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro