Cogs and Levers A blog full of technical stuff

Getting Started with ClojureScript

Introduction

I recently decided to dip my toes into ClojureScript. As someone who enjoys exploring different language ecosystems, I figured getting a basic “Hello, World!” running in the browser would be a fun starting point. It turns out that even this small journey taught me quite a bit about how ClojureScript projects are wired together.

This post captures my first successful setup: a minimal ClojureScript app compiled with lein-cljsbuild, rendering output in the browser console.

A Rough Start

I began with the following command to create a new, blank project:

lein new cljtest

First job from here is to organise dependencies, and configure the build system for the project.

project.clj

There’s a few things to understand in the configuration of the project:

  • We add org.clojure/clojurescript "1.11.132" as a dependency
  • To assist with our builds, we add the plugin lein-cljsbuild "1.1.8"
  • The source path is normally src, but we change this for ClojureScript to src-cljs
  • The output will be javascript output for a website, and all of our web assets go into resources/public
(defproject cljtest "0.1.0-SNAPSHOT"
  :min-lein-version "2.9.1"
  :description "Minimal ClojureScript Hello World"
  :dependencies [[org.clojure/clojure "1.11.1"]
                 [org.clojure/clojurescript "1.11.132"]]
  :plugins [[lein-cljsbuild "1.1.8"]]
  :source-paths ["src-cljs"]
  :clean-targets ^{:protect false} ["resources/public/js" "target"]

  :cljsbuild
  {:builds
   {:dev
    {:source-paths ["src-cljs"]
     :compiler {:main cljtest.core
                :output-to "resources/public/js/main.js"
                :output-dir "resources/public/js/out"
                :asset-path "js/out"
                :optimizations :none
                :source-map true
                :pretty-print true}}

    :prod
    {:source-paths ["src-cljs"]
     :compiler {:main cljtest.core
                :output-to "resources/public/js/main.js"
                :optimizations :advanced
                :pretty-print false}}}})

We have two different build configurations here: dev and prod.

The dev configuration focuses on being much quicker to build so that the change / update cycle during development is quicker. Source maps, pretty printing, and no optimisations provide the verbose output appropriate for debugging.

The prod configuration applies all the optimisations. This build is slower, but produces one single output file: main.js. This is the configuration that you use to “ship” your application.

Your First ClojureScript File

Place this in src-cljs/cljtest/core.cljs:

(ns cljtest.core)

(enable-console-print!)
(println "Hello from ClojureScript!")

HTML Page to Load It

Create a file at resources/public/index.html:

<!doctype html>
<html>
  <head><meta charset="utf-8"><title>cljtest</title></head>
  <body>
    <h1>cljtest</h1>
    <script src="js/out/goog/base.js"></script>
    <script src="js/main.js"></script>
    <script>goog.require('cljtest.core');</script>
  </body>
</html>

Build & Run

Compile your dev build:

lein clean
lein cljsbuild once dev

Then open resources/public/index.html in your browser, and check the developer console — you should see your message.

If you want to iterate while coding:

lein cljsbuild auto dev

When you’re ready to build a production bundle:

lein cljsbuild once prod

Then you can simplify the HTML:

<script src="js/main.js"></script>

No goog.require needed — it all gets bundled.

Step it up

Next, we’ll step up to something a little more useful. We’ll put together a table of names that we can add, edit, delete, etc. Just a really simple CRUD style application.

In order to do this, we’re going to rely on a pretty cool library called reagent.

We add the following dependency to project.clj:

[reagent "1.0.0"]

State

Our little application requires some state:

(defonce names (r/atom [{:id 1 :name "Alice"}
                        {:id 2 :name "Bob"}]))

(defonce next-id (r/atom 3))
(defonce editing-id (r/atom nil))
(defonce edit-text (r/atom ""))

names is the currentl list of names. next-id gives us the next value that we’ll use an ID when adding a new record. editing-id and edit-text manage the state for updates.

Table

We can now render our table using a simple function:

(defn name-table []
  [:div
   [:h2 "Name Table"]
   [:table
    [:thead
     [:tr [:th "Name"] [:th "Edit"] [:th "Delete"]]]
    [:tbody
     (for [n @names]
       ^{:key (:id n)} [name-row n])]]
   [:div
    [:input {:placeholder "New name"
             :value @edit-text
             :on-change #(reset! edit-text (.. % -target -value))}]
    [:button {:on-click
              #(when-not (clojure.string/blank? @edit-text)
                 (swap! names conj {:id @next-id :name @edit-text})
                 (swap! next-id inc)
                 (reset! edit-text ""))}
     "Add"]]])

The table renders all of the names, as well and handles the create case. The edit case is a little more complex and requires a function of its own. The name-row function manages this complexity for us.

(defn name-row [{:keys [id name]}]
  [:tr
   [:td name]
   [:td
    (if (= id @editing-id)
      [:<>
       [:input {:value @edit-text
                :on-change #(reset! edit-text (.. % -target -value))}]
       [:button {:on-click
                 (fn []
                   (swap! names (fn [ns]
                                  (mapv (fn [n]
                                          (if (= (:id n) id)
                                            (assoc n :name @edit-text)
                                            n))
                                        ns)))
                   (reset! editing-id nil))}
        "Save"]]
      [:<>
       [:button {:on-click #(do (reset! editing-id id)
                                (reset! edit-text name))}
        "Edit"]])]
   [:td
    [:button {:on-click
              (fn []
                (swap! names (fn [ns]
                               (vec (remove (fn [n] (= (:id n) id)) ns)))))} ;; FIX
     "Delete"]]])

Mounting!

Now we’re going to make sure that these functions end up on our web page.

(defn mount-root []
  (dom/render [name-table] (.getElementById js/document "app")))

(defn init []
  (enable-console-print!)
  (mount-root))

We need an app element in our HTML page.

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>cljtest</title>
  </head>
  <body>
    <h1>cljtest</h1>

    <!-- This is our new element! -->
    <div id="app"></div>

    <script src="js/out/goog/base.js"></script>
    <script src="js/main.js"></script>
    <script>goog.require('cljtest.core'); cljtest.core.init();</script>

  </body>
</html>

Conclusion

This journey started with a humble goal: get a simple ClojureScript app running in the browser. Along the way, I tripped over version mismatches, namespace assumptions, and nested anonymous functions — but I also discovered the elegance of Reagent and the power of functional UIs in ClojureScript.

While the setup using lein-cljsbuild and Reagent 1.0.0 may feel a bit dated, it’s still a solid way to learn the fundamentals. From here, I’m looking forward to exploring more advanced tooling like Shadow CLJS, integrating external JavaScript libraries, and building more interactive UIs.

This was my first real toe-dip into ClojureScript, and already I’m hooked. Stay tuned — there’s more to come.