Start a ClojureScript App from Scratch

Welcome to my step by step guide to setting up a ClojureScript app. We'll work to assuage your fears & stresses and alleviate those nagging thoughts about doing things the "right" or "wrong" way.

In this post we will walk through a battle tested approach to setting up a Clojure(Script) app and describe the rationale for each decision we make.

Setup Project Structure

This section is about the folder structure of our application. Whenever I name a Clojure(Script) project I will use the company name and then the app name to inform the naming of our app's folders.

For example, we can pretend that our company is called tallex and we are building an app called time dive.

Step 1 - Add the Files and Folders

With this in mind, go ahead and create each file and folder exactly as seen below:

.
├── README.md
├── resources
│   ├── index.html
│   └── style.css
├── src
│   └── tallex
│       └── time_dive.cljs
└── tests
    └── tallex
        └── time_dive_tests.cljs

Step 2 - Add HTML

Time for some code. We are building a web app and like all web apps we need HTML. HTML is the "bones" of your web app. Open your index.html file and add the following:

<!DOCTYPE html>
<html>
  <head>
    <title>Time Dive</title>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <h1 class="site__title">
      <span class="site__title-text">Time Dive</span>
    </h1>
    <script src="/out/main.js"></script>
  </body>
</html>

Step 3 - Add CSS

Next up, we give our app some "clothes": CSS. Open the style.css file and slam down these styles:

:root {
  --color-purple: rgba(197, 18, 193, 1);
  --color-pink: rgba(241, 50, 50, 1);
}

body {
  margin: 0;
  height: 100vh;
  display: flex;
  font-family: Arial;
  align-items: center;
  justify-content: center;
}

.site__title {
  font-size: 100px;
  width: 50%;
  text-align: center;
}

.site__title-text {
  background: -webkit-linear-gradient(
    34deg,
    var(--color-purple),
    var(--color-pink)
  );
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

Step 4 - Add ClojureScript

Now we finally get to write some ClojureScript. Open time_dive.cljs and sprinkle down these codes:

(ns tallex.time-dive)

(js/console.log "Hello, Time Dive!")

The above defines a namespace called tallex.time-dive. This namespace is going to be the entry point of our app.

Step 5 - Add ClojureScript Tests

Good tests are the Alfred to a software engineers Batman. Open up that time_dive_tests.cljs file and add the following:

(ns tallex.time-dive-tests)

(js/console.log "Hello, Time Dive Tests!")

This concludes setting up the project structure and boilerplate code. The next step is about setting ourselves up with tools we need to run our Clojure(Script) app.

Running and Developing ClojureScript

To develop and run our app the most popular tools in Clojure land are lein, boot and clj. The good news is you only need to choose one of these. Before I reveal which we are going to use, let me provide a quick overview of each of them.

lein is the grandfather and most popular Clojure(Script) build tool. If I had to compare it to something it would be as if npm and webpack made a Clojure baby. You will see lein used in most all projects created prior to mid-2018. Yet, it has started to show its age. So members of the Clojure(Script) community went off to build a better lein and they called it boot.

boot is a definite improvement over lein. It learned from many of lein's shortcomings and managed to gather the attention of a strong minority of Clojure(Script) developers.

clj, also known as the clojure cli tool, is magicked down to us by the maintainers of Clojure. It was initially met with confusion, but over the past year has come to be seen by many, including myself, as the tool to run and develop our clojure(script) programs. Where lein and boot are complex and often reach in their scope, the clj tool is about 3 things:

  • run clojure programs
  • resolve transitive dependencies
  • build classpaths

Ultimatley, clj is simple, oriented towards beginners and yet powerful enough to support advanced users. If you're interested in learning more, checkout out the clojure tools post. Also feel free to checkout the 2021 State of Clojure Community Report to see which tools the Clojure community uses most.

Step 6 - Add deps file

To run our Clojure(Script) project with clj we first need to setup our deps.edn file. Start by creating a deps.edn file in the root of our project and then make it look like this:

{:paths
 ["src" "tests" "resources"]

 :deps
 {org.clojure/clojurescript {:mvn/version "1.10.866" }}

 :aliases
 {:dev {:main-opts ["-m"  "cljs.main"
                    "-ro" "{:static-dir,[\".\",\"out\",\"resources\"]}"
                    "-w"  "src"
                    "-c"  "tallex.time-dive"
                    "-r"]}}}

Before we continue we should be familiar with what we're doing in our deps.edn file:

  • :paths tells clj where to look for clojure code. Also known as a classpath
  • :deps tells clj which dependencies our app needs. Right now our only dependency is ClojureScript.
  • :aliases are like shortcuts. We can store long commands, or alternate dependencies in these.

When we run our app for prod, dev or test we may need to run the app differently. That is why we use aliases. In our case, we specified a :dev alias and configured it to:

  • "-m" run cljs.main (run clojurescript).
  • "-ro" teach the repl where to find static files e.g. html, css etc.
  • "-w" watch all files in src dir and recompile when they are changed.
  • "-c" compile our app: tallex.time-dive.
  • "-r" run a REPL and connect it to the browser.

Now that we have a rough idea of what is going on we are ready to take our app for a test drive. Open a terminal, move into the root of the app and run the following command:

clj -M:dev

wait a bit and :dev will compile our code, automatically open a browser tab and present you with what we have so far:

screenshot of time dive app

Before we jump over to the next section I want to draw your attention to the fact that we have zero dependencies. Think about this: our files are being watched, code is recompiled on save, we are greeted with a browser repl and all of this with zero dependencies. Yes, we are still missing a few niceties, but we are not done yet. We will get you to hero toolchain status soon.

Setup a ClojureScript Toolchain

As I said earlier, we have zero dependencies and already have a powerful toolchain. This section will take our toolchain to another level so we can achieve parity to JavaScript standards by introducing just one tool: Figwheel.

figwheel is a popular ClojureScript tool and a must have for my workflow. Figwheel has many features, but we are only going to focus on the ones that allow us to sync with what JavaScript land is used to. These include:

  • live ClojureScript and CSS Reloading
  • Informative error messages
  • Build configurations for prod, dev, test et al.

Now that we know what figwheel does, let's see how to make it do the things.

Step 7 - Add Figwheel

It starts by adding figwheel as a dependency. We do this by opening the deps.edn file and add the line you see highlighted below:

; highlight-range{7}
{:paths
 ["src" "tests" "resources"]

 :deps
 {org.clojure/clojurescript {:mvn/version "1.10.866"}

  com.bhauman/figwheel-main {:mvn/version "0.2.13"}}

 ; ...
 }

Step 8 - Add build configuration

A build configuration is where we specify figwheel and ClojureScript compiler options. For this guide we will create a development build configuration.

Create a new file in the root of our ClojureScript app called dev.cljs.edn and add the following code to it:

^{:watch-dirs ["src"]
  :css-dirs   ["resources"]}
{:main tallex.time-dive}

Okay. We have specified build configuration options. What does all of it mean?

  1. :watch-dirs - when any cljs files change in src directory figwheel recompiles and re-loads the browser.
  2. :css-dirs - when css files change in the resources directory figwheel recompiles and re-loads them in the browser
  3. :main - This is an option that figwheel passes to the ClojureScript compiler which tells the compiler which file is our apps entry point.

As you may have guessed, Fighwheel is going to replace the need for us to use -w, -r and -c above. It is a separate program that provides richer versions of those built-in commands. With our dev build configuration in place, we have one last step.

Step 9 - Update :dev Alias

The command we were using to run our app, clj -M:dev, is still not using figwheel yet. Open deps.edn and make it look like this:

{:paths
 ["src" "tests" "resources"]

 :deps
 {org.clojure/clojurescript {:mvn/version "1.10.866"}

  com.bhauman/figwheel-main {:mvn/version "0.2.13"}}

 :aliases
 {:dev {:main-opts ["-m"  "figwheel.main" "--build" "dev" "--repl"]}}}

Time for sanity check to make sure everything is still working. Run the following command:

clj -M:dev

Your app should run, but you will notice that you get the following screen:

screenshot of figwheel landing page

This appears because figwheel is looking for our index.html in resources/public. Up until now we put our index.html file directly under the resources dir. This means we have to change our folder structure a little.

Step 10 - Restructure resources dir

Go ahead and add a public directory to the resources dir and move your index.html and style.css into it. Your folder structure should look like this:

.
├── README.md
├── deps.edn
├── dev.cljs.edn
├── resources
│   └── public
│       ├── index.html
│       └── style.css
├── src
│   └── tallex
│       └── time_dive.cljs
└── tests
    └── tallex
        └── time_dive_tests.cljs

We also have to update the script tag in our index.html file.

<script src="/cljs-out/dev-main.js"></script>

time to run the app again:

clj -M:dev

The toolchain is now in place, but we are still missing the great and powerful Hot Module Reloading.

Hot Module Reloading

Hot Module Reloading (HMR) is the holy grail of React development. So much so that it is often spoken in the same breath as if the two are co-dependent, yet the two are not joined in any way.

As long as you write reloadable code you can take advantage of HMR. This is the catch though: writing reloadable code can be tricky and time consuming. Writing reloadable code is made much easier when you write your code using React. This is likely part of the reason the two are seen as linked.

The point is that figwheel offers a framework / library agnostic mechanism to support your ability to write HMR in your toolchain.

Specifically, figwheel gives us hooks which we specify in the namespace where we want to take advantage of HMR. The way it works is when we write HMR we have to tell our app how to tear itself down and setup again when files are re-compiled. We have to write these functions ourselves. What figwheel helps with is providing hooks like :after-load and :before-load which will call our setup and teardown functions.

Step 11 - Refactor for Figwheel

We can demonstrate how to use figwheel hooks in the time-dive namespace. Our time-dive namespace is logging "Hello, Time Dive" on each reload. We could however only have it log on re-compile by adding a hook like this:

(ns ^:figwheel-hooks tallex.time-dive)

(defn ^:after-load re-render []
  (js/console.log "Hello, Time Dive!"))

Now when you try to run the app you will notice that the console.log does not log the first time, but only after each save. Things to take note of:

  • [^:figwheel-hooks] - meta data telling figwheel we want to use hooks in our namespace
  • [^:after-load] - meta data telling figwheel that we want it to run the function, re-render, after each compile

This is a rich topic so my hope is that I was able to illustrate the fact that HMR and React are not linked, and provide a little insight into how you can use this feature outside of React.

Add Reagent

You could use most any JS framework in ClojureScript, but the ClojureScript community loves React and the community has made wrappers to make React easier to use in ClojureScript. While there are many wrappers the most popular React wrapper is currently Reagent so we will show you how to use that.

Step 12 - Refactor for Reagent

In addition to adding Reagent we are going to update our deps.edn, html, css and cljs. Start by opening the deps.edn file and making it look like this:

; highlight-range{9}
{:paths
 ["src" "tests" "resources"]

 :deps
 {org.clojure/clojurescript {:mvn/version "1.10.866"}

  com.bhauman/figwheel-main  {:mvn/version "0.2.13"}

  reagent                   {:mvn/version "1.1.0"}}


 :aliases
 {:dev {:main-opts ["-m"  "figwheel.main" "--build" "dev" "--repl"]}}}

Next we can open our index.html file and modify as follows:

<!-- highlight-range{8,10} -->
<!DOCTYPE html>
<html>
  <head>
    <title>Time Dive</title>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <div id="root"></div>

    <script src="/cljs-out/dev-main.js"></script>
  </body>
</html>

Finally open our time-dive namespace and add a few things:

(ns ^:figwheel-hooks tallex.time-dive
  (:require
    [reagent.dom :as r.dom]))


(defn app []
  [:h1.site__title
    [:span.site__title-text "Time Dive"]]])


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


(defn ^:after-load re-render []
  (mount))


(defonce start-up (do (mount) true))

A little about the above:

  • app is our first example of a Reagent component
  • mount a function. When called, it will display our time-dive app
  • re-render a function with a hook. When called, it reloads our app. It supports the HMR part
  • defonce is used to control side effects.

Last step: open up style.css and change the body tag to #root

/* replace ... */
body {
  /* ... */
}

/* with ... */
#root {
  /* ... */
}

Ready to see if it all worked? Run clj -M:dev and marvel at your ClojureScript SPA.

Conclusion

At this point we have created a ClojureScript app from scratch and provided a solid foundation which should allow you to take this app in any direction you like. Of course, I did not go into details beyond the initial phase, but if you are interested in possible next steps or sources for inspiration, here are a few that I often recommend.

These resources are great next steps for learning to work with Clojure(Script).