Getting Started with ClojureScript

TLDR: clone this repo to get started, or this repo for a more involved example (the latter is also live here)


Clojure is famous for being amazing and a total bliss in every way... except when it comes to documentation. This is especially true of the official ClojureScript website: the Quick Start guide doesn't even mention HTML, even though the idea is to compile ClojureScript to JavaScript and then reference it in an HTML file.

Let's see how we can build, step by step, a blissful ClojureScript development experience, and then we'll look at the benefits of using this weird, funny-looking, parentheses-heavy (there's a point behind them, I swear!) language.

Intended Audience

Programmers familiar with (front end) development who'd like to code like it's 2020 – not 1999. If being able to save the state of your app (and modify it on the fly) between hot reloads sounds good, read on.

Step 0: Prerequisites

You'll need the following tools available on your PATH:

If you're on a Mac, both are also available via brew.

Now we can go ahead and create a folder for our project:

mkdir clojurescript-template

Step 1: HTML

We'll start with HTML. Create a folder called public with the following index.html:

<body>
  <div id="app"></div>
  <script src="js/main.js"></script>
</body>
</html>

This should look familiar to React, etc. developers. We'll manipulate the DOM from our compiled main.js and we'll render the whole app into that one div.

Step 2: Shadow CLJS and Deps

shadow-cljs provides everything you need to compile your ClojureScript code with a focus on simplicity and ease of use.

Pretty much.

Let's install it first:

echo '{}' > package.json # we'll only be using package.json for dependency management and scripts. Feel free to add other fields (name, author, etc.)
npm i -D shadow-cljs

And then initialize a config file:

./node_modules/.bin/shadow-cljs init

This will create a placeholder shadow-cljs.edn which looks like this:

;; shadow-cljs configuration
{:source-paths
 ["src/dev"
  "src/main"
  "src/test"]

 :dependencies
 []

 :builds
 {}}

As you can see, shadow-cljs can manage dependencies and the source code, but we'll use the now official Clojure tool for that called Deps, mostly because at least one popular Clojure setup, IntelliJ + Cursive, does not yet support shadow-cljs.

So, let's create a deps.edn file and make it look like this:

{:paths ["src/dev"]

 :deps  {thheller/shadow-cljs {:mvn/version "2.8.94"}}

 :aliases
 {:cljs
  {:extra-deps {}
   :extra-paths ["src/main" "src/test"]}}}

Under :aliases we'll have a map of, well, aliases. In this case, we want shadow-cljs to know about some :extra-deps (currently empty) and :extra-paths. We'll call that alias :cljs.

Now back to shadow-cljs.edn:

{:deps
 {:aliases [:cljs]}

 :builds
 {}}

Here we tell it to use Deps (via the :deps key) to manage dependencies and source paths.

Step 3: Hello, World

It's about time. Create the following folders: src/main/foo. Inside foo, create core.cljs:

(ns foo.core)

(js/alert "Hello, World")

(println "Hello, World")

Now that we have some ClojureScript code, we'll need to compile it and place it under public/js/main.js (since that's where our index.html is pointing to).

Back to shadow-cljs.edn:

{:deps
 {:aliases [:cljs]}

 :builds
 {:app {:target :browser
        :output-dir "public/js"
        :asset-path "js"
        :modules {:main {:entries [foo.core]}}}}}

See the :app? That's how we'll refer to this build configuration when issuing commands to shadow-cljs (more on that in a bit).

We're ready to go. Let's define helper scripts in package.json (feel free to skip this step and issue relevant commands directly):

{
  "scripts": {
    "server": "shadow-cljs server",
    "app:compile": "shadow-cljs compile app",
    "app:release": "shadow-cljs release app"
  },
  "devDependencies": {
    "shadow-cljs": "^2.8.94"
  }
}

Our workflow is the following: have one shell session dedicated to running a shadow-cljs server (npm run server). That way, all the other shadow-cljs commands will use that server instance and take much less time to complete. There's even a dashboard you can navigate to:

shadow-cljs - server version: 2.8.94 running at http://localhost:9630

Now that we have our server running, try compiling the cljs code:

npm run app:compile

You'll see the output in public/js. Now start any static web server like

python -m SimpleHTTPServer

or skip to the next section where we'll have shadow-cljs do that for us, and navigate to index.html. You should see a Hello, World popup and a message in the console.

Step 4: All the Good Things

Now – to the cool bits. We'll start slowly and build them up, step by step.

To start with, add this new script to package.json:

{
  "scripts": {
    "server": "shadow-cljs server",
    "server:repl": "shadow-cljs clj-repl",
    "app:compile": "shadow-cljs compile app",
    "app:release": "shadow-cljs release app"
  },
  "devDependencies": {
    "shadow-cljs": "^2.8.94"
  }
}

Run it in a new terminal session:

npm run server:repl

You'll see a prompt this time around:

shadow-cljs - REPL - see (help)
To quit, type: :repl/quit
shadow.user=>

For now, let's treat this prompt like any other prompt from any other REPL (node, etc.). Type the following:

shadow.user=> (ns-aliases *ns*)

Which gives us

{shadow "shadow.cljs.devtools.api"]}

That's helpful: we can type shadow instead of shadow.cljs.devtools.api to use functions from that namespace. What functions could those be? Let's try compiling our app again:

shadow.user=> (shadow/compile :app)
[:app] Compiling ...
[:app] Build completed. (43 files, 0 compiled, 0 warnings, 0.85s)
:done

That's neat: we can do everything from this REPL. Let's add two more scripts to package.json (at this point, more for reference's and completeness' sake than anything else):

"app:watch": "shadow-cljs watch app",
"app:repl": "shadow-cljs cljs-repl app"

Back in our repl, let's try

shadow.user=> (shadow/watch :app)
[:app] Configuring build.
[:app] Compiling ...
[:app] Build completed. (135 files, 1 compiled, 0 warnings, 0.93s)
:watching

This is the same as running npm run app:watch, only, as we just saw with (shadow/compile :app), we can do it from the REPL instead, which gives us a lot more control and flexibility (we'll talk more about it during Coffee Break).

watch is a live-reload feature (again, nothing new so far, but we are going slowly after all). Let's briefly go back to shadow-cljs.edn file and add :dev-http:

{:deps
 {:aliases [:cljs]}

 :dev-http {9000 "public"}
  
 :builds
 {:app {:target :browser
        :output-dir "public/js"
        :asset-path "js"
        :modules {:main {:entries [foo.core]}}}}}

That's the static server we talked about previously. Our running server will pick up the changes to the config automatically and display this:

shadow-cljs - HTTP server available at http://localhost:9000

Let's see the live-reload in action while we're at it. Open localhost:9000, change your core.cljs (maybe remove that annoying alert) and save it. You should see a logo in the left-hand corner, and your changes will be picked up and reloaded automatically.

Now to the app:repl script that we added. Go back to the server repl you still have prompting you for input and type:

shadow.user=> (shadow/repl :app)
cljs.user=>

The current namespace changed to cljs.user. Also, our REPL is connected to the browser now! Type

cljs.user=> (println 42)
42
nil

42 is printed in our REPL running in a terminal session. nil is the returned value of the (println 42) expression. But look at the console on localhost:9000 and you'll find that 42 made its way there as well.

Let's try something a bit more interesting:

=> (def hello-world (doto (.createElement js/document "p") (aset "innerHTML" "Hello, World")))
#'cljs.user/hello-world
=> (def app (.getElementById js/document "app"))
#'cljs.user/app
=> (.appendChild app hello-world)

Look in your browser – now that's a proper Hello, World example!

Step 5: The State of the World

Now for the big finish. Let's add some state and UI to core.cljs:

(ns foo.core)

;; STATE

(defonce state (atom 0))

;; HELPERS

(defn root []
  (.getElementById js/document "app"))

(defn counter []
  (.getElementById js/document "counter"))

(defn replaceChild [root new-child old-child]
  (.replaceChild root new-child old-child))

;; COMPONENTS

(defn new-counter []
  (doto (.createElement js/document "p")
    (.setAttribute "id" "counter")
    (aset "innerHTML" @state)))

(defn new-button-inc []
  (doto (.createElement js/document "button")
    (aset "innerHTML" "+1")
    (aset "onclick" (fn [_event]
                      (swap! state inc)
                      (replaceChild (root) (new-counter) (counter))))))

(defn new-button-reset []
  (doto (.createElement js/document "button")
    (aset "innerHTML" "reset")
    (aset "onclick" (fn [_event]
                      (reset! state 0)
                      (replaceChild (root) (new-counter) (counter))))))

;; RENDER

(defn render [root & components]
  (doseq [c components]
    (.append root c)))

(defn ^:export init
  []
  (aset (root) "innerHTML" "")
  (render (root) (new-counter) (new-button-inc) (new-button-reset)))

Save the file and issue the following commands in the REPL:

cljs.user=> (in-ns 'foo.core)
nil
foo.core=> (init)
nil

First, we're switching to the foo.core namespace (so we wouldn't have to prefix every command from that namespace, i.e., we could've instead typed (foo.core/init))

Then, we call the init function, which renders our UI. Make sure that the buttons work, etc.

Now, once you're ready, click reset and type @state in the REPL. You should get 0. That's the current value of our state.

Now, type this:

foo.core=> (reset! state 42)
42
foo.core=> (init)
nil

That's interesting: we changed the state from our REPL, and now, re-rendering the UI, the changes are persisting.

Of course, this is still inefficient – there's too much manual work.

Let's keep modifying shadow-cljs.edn:

{:deps
 {:aliases [:cljs]}

 :dev-http {9000 "public"}

 :builds
 {:app {:target :browser
        :output-dir "public/js"
        :asset-path "js"
        :modules {:main {:entries [foo.core]
                         :init-fn foo.core/init}}
        :devtools {:repl-init-ns foo.core
                   :after-load foo.core/init}}}}

:init-fn specifies the function that will be called when the module is loaded initially (refresh localhost:9000 to see it in action)

:repl-init-ns will switch the current namespace to foo.core when we connect to a running CLJS repl

:after-load specifies the function to run each time we're live-reloading

Now go back to the page and give +1 a few clicks. Then modify some part of UI in core.cljs. For instance, replace (aset "innerHTML" "+1") with (aset "innerHTML" "plus one") and save the file. Did you notice that the UI has changed while the state stayed the same?

We're not stopping here, of course. Up until now, we've been using vanilla JavaScript from ClojureScript to render our UI. Now we're going to move to a de-facto standard in the ClojureScript world when it comes to framework-less UI development: Reagent.

Step 6: Reagent

Reagent bills itself as

Minimalistic React for ClojureScript

Let's bring it in first via deps.edn:

{:paths ["src/dev"]

 :deps  {thheller/shadow-cljs {:mvn/version "2.8.94"}}

 :aliases
 {:cljs
  {:extra-deps {reagent {:mvn/version "0.10.0"}}
   :extra-paths ["src/main" "src/test"]}}}

Every time we bring in an external dependency, we have to restart the server. So, kill your REPL and server and run npm run server. This will install Reagent (and React, too!).

Now let's go back to where we were. You can repeat the steps above or run the scripts we've defined (in two separate terminal sessions – another reason to prefer doing everything from the REPL):

npm run app:watch
npm run app:repl

In core.cljs:

(ns foo.core
  (:require [reagent.core :as r]
            [reagent.dom :as rdom]))

;; STATE

(defonce state (r/atom 0))

;; COMPONENTS

(defn counter []
  [:p "Counter: " @state])

(defn button-inc []
  [:input {:type "button"
           :value "+1"
           :on-click (fn [_event]
                       (swap! state inc))}])

(defn button-reset []
  [:input {:type "button"
           :value "reset"
           :on-click (fn [_event]
                       (reset! state 0))}])

(defn container []
  [:div
   [counter]
   [button-inc]
   [button-reset]])

;; RENDER

(defn root []
  (.getElementById js/document "app"))

(defn ^:export init []
  (rdom/render [container] (root)))

Looks much cleaner, doesn't it? Save, refresh localhost:9000 and click around a bit.

From your REPL:

foo.core=> (reset! state 42)

This changes the state and you can see the update in the UI.

In core.cljs, replace

:value "+1"

with

:value "plus one"

and save. Neat, huh? The state stays with us, and is completely under our control, while we're free to change the UI in any way we want.

~~~ Coffee Break ~~~

Naturally, this is just the tip of the iceberg. We came nowhere near a real REPL-driven experience in this tutorial, but hopefully it gave you a taste of what's possible.

The eventual idea is to connect your editor to a running REPL and evaluate everything from the editor. Once you experience this kind of development, where feedback is immediate and control over state during runtime is as fine-grained as you want, it's really difficult to see the merits of the usual change->save->compile->check->debug->change->etc. cycle.

Parentheses

Let's quickly touch on them. Clojure is a LISP. Like any LISP, it has no syntax – only data structures. List is one of them. Its literal representation is (). There are several ways to create one. Let's use the list function (we'll issue the following commands in the server REPL, so start one with npm run server:repl):

shadow.user=> (list 1 2)
(1 2)
shadow.user=> (type (list 1 2))
clojure.lang.PersistentList

The list we've created is fully persistent; the implementation of clojure.lang.PersistentList (in Java) can be found here. Note that a function invocation is also a list:

=> (+ 1 2)
3
=> (list + 1 2)
(#object[clojure.core$_PLUS_ 0x74a72a9d "clojure.core$_PLUS_@74a72a9d"] 1 2)
=> (eval (list + 1 2))
3

What if we tried (1 2)?

=> (1 2)
Execution error...
class java.lang.Long cannot be cast to class clojure.lang.IFn

So 1, which is java.lang.Long, does not implement clojure.lang.IFn – we cannot invoke 1. What this gives us, the fact that function invocations are lists themselves, is that we end programming in data structures themselves – for all intents and purposes, we program in Abstract Syntax Trees, which are inaccessible to us in many other languages. This gives us, users, the ability to extend the language which usually only the creators of a language have.

Let's look at two examples. We'll start with a simple one:

shadow.user=> (defmacro debug-plus-10 [expr]
  (let [new-expr (concat expr (list 10))]
    (println "debugging " new-expr)
    new-expr))

The ability to access the data structures is realized with macros. Here, we define one with defmacro. Clojure is an eagerly-evaluated language, but macros don't evaluate their arguments. Instead, they return the data structure that will then be evaluated.

In this case, debug-plus-10 expects an expression expr. On the next line, we concat 10 to that expression. Then we print the expression out and return it. Only then will the expression be evaluated.

Let's try it out:

=> (debug-plus-10 (+ 1 2))
debugging  (+ 1 2 10)
13

(+ 1 2) is the argument we pass to debug-plus-10. Since it's a list like any other, we can use all the rich sequence API on it. In this case, we use concat to append 10 to the end of the list. We print it out and return the resulting list (+ 1 2 10), which then gets evaluated to 13.

And now for something slightly different. Consider C#'s using statement:

using (var font1 = new Font("Arial", 10.0f))
{
    byte charset = font1.GdiCharSet;
}

Font implements IDisposable. using calls Dispose on the object correctly. But read this:

You can achieve the same result by putting the object inside a try block and then calling Dispose in a finally block; in fact, this is how the using statement is translated by the compiler. The code example earlier expands to the following code at compile time (note the extra curly braces to create the limited scope for the object):

{
  var font1 = new Font("Arial", 10.0f);
  try
  {
    byte charset = font1.GdiCharSet;
  }
  finally
  {
    if (font1 != null)
      ((IDisposable)font1).Dispose();
  }
}

If there were no using in C#, you'd have to wait for the designers to put it in. In Clojure, we don't have to wait! Observe:

=> (defmacro using [resource & body]
  `(let [~(get resource 0) ~(get resource 1)]
     (try
      ~@body
      (finally
       (println "getting ready to close stream on file" ~(last (get resource 1)))
       (.close ~(get resource 0))))))

The backtick ` means, don't evaluate the following data structure right now, but return it to be evaluated at the call site. The tilde ~ means, do evaluate the following data structure right now.

Let's try this macro out:

=> (using [fr (new java.io.FileReader "deps.edn")] (println "first char is:" (char (.read fr))))
first char is: {
getting ready to close stream on file deps.edn
nil

To sum up,

There's much more to Clojure itself:

Read more here.

Of course, Clojure is not a magic bullet and has issues:

Despite them, I encourage you to give Clojure(Script) a shot.

REPL in Space

Here's a talk by Dr. Ron Garret on LISP at NASA and debugging a space craft million miles away from Earth using a REPL running on the space craft.

Step 7: What's Next?

Setting up a proper development environment is crucial to a blissful Clojure(Script) experience. You'll be hooking up to running REPLs and evaluating your code straight from the editor! Here are some options:

You probably see one of your favourite editors in this list – go with it! I've been using IntelliJ and Emacs – both work great. I've heard good things about VSCode and Vim, too.

If you, like me, learn by reading books, try Clojure for the Brave and True and Programming Clojure.

For next steps in front end with ClojureScript, one of the more popular options is re-frame.

Step 8: Addendum

There are some thing I'd like to add to this ClojureScript template we've been working on. They are not important enough to be highlighted above, but deserve a mention nonetheless.

Adding a new dependency: binaryage/devtools {:mvn/version "1.0.0"}. It will make ClojureScript values we output to the Chrome DevTools console look nicer.

It's nice to have a helper function.

Have our server nREPL always start on port 9001 and switch to utils namespace we just defined.

Clone it!

The final result is on GitHub. Hack away!

Morrissey I Wish You Lonely

Fyodor Dostoevsky The Brothers Karamazov

Back to Posts