Between Two Parens

Start a ClojureScript App from Scratch

posted on

Building apps is hard AF. Choosing a language like Clojure(Script) can remove some of the pain, but the fact remains that when we are starting out, just knowing where to begin is a frustrating experience.

My goal is to provide comfort, guidance and context into the initial phases of writing a ClojureScript app. We'll work to assuage your fears & stresses and alleviate those overwhelming thoughts about doing things the right or wrong way to get you to a point where you can do what matters: writing sweet sweet Clojure(Script).

Through this guide we will cover the steps I take when starting a new ClojureScript app from scratch.

To get us into the headspace, imagine that we are a company called tallex and we are building an app called time dive. This excercise will prove useful when learning how to structure/name our files/folders.

Setup Project Structure

Generally speaking, Clojure(Script) web apps look like this:

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

The goal of this section is to build out the above folder structure.

Step 1 - Add the Files and Folders

Go ahead and create each file and folder exactly as seen in the above code block.

Step 2 - Add HTML

This is a browser app and like all browser apps the HTML is the "bones". 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: the "clothes" of our app: CSS. Open style.css 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 to configure our build tools.

Build Tools

To run, develop and build a production version of our app we need what developers call build tools. The most popular build tools in Clojure land are lein, boot and clj. Before I reveal which tool we are going to use, let me provide a quick rundown 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 has learned from many of lein's shortcomings and managed to gather the attention of a strong minority of Clojure(Script) developers. Even so, the feeling can often be that boot does not offer enough to trade in lein.

clj 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 build tool we deserve. Instead of following in the path of lein or boot the goal of clj is do three things well:

  1. run clojure programs
  2. manage dependencies
  3. build classpaths

what makes clj special is that it's begineer oriented and also powerful enough for advanced Clojurists. Further, because clj is focused on doing less it's easier to understand which means that when things go wrong it's easier to debug. It also encourages us to compose libraries instead of buying into a framework.

As you may have guessed, clj is my build tool of choice and what we will use in this app. If you are the type who is looking for a deeper understanding of these build tools Sean Corfield has written an excellent overview of clojure's build tools. Further, if you are interested in the usage breakdown of the build tools checkout the 2019 State of Clojure Community Report

Step 6 - Add deps file

To use clj we need to configure it. clj is configured using a file called deps.edn. Begin 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.520"}}

 :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 the configuration file is doing:

  • :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 commands we can write to modify how we build our app.

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 clojurescript.
  • "-ro" teach the repl where to find static files like html and css.
  • "-w" watch all files in our src dir for changes and recompile on save.
  • "-c" run our app: tallex.time-dive.
  • "-r" run a browser connected REPL.

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 -A: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:

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

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

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

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.

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 -A: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.520"}}

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

 :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 -A: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 -A: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 eaiser when your 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 Reagent

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 we specified above, 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 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:

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

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

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

  reagent                   {:mvn/version "0.8.1"}}

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

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

<!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.core :as r]))


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


(defn mount []
  (r/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 will display our time-dive app
  • re-render a function with a hook. When called, reloads our app. It supports the HMR part

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 -A: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).