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 aclasspath
- :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:
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?
- :watch-dirs - when any
cljs
files change insrc
directory figwheel recompiles and re-loads the browser. - :css-dirs - when
css
files change in theresources
directory figwheel recompiles and re-loads them in the browser - :main - This is an option that figwheel passes to the
ClojureScript compiler
which tells the compiler which file is our appsentry 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:
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 componentmount
a function. When called, it will display ourtime-dive
appre-render
a function with a hook. When called, it reloads our app. It supports the HMR partdefonce
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.
- Example Reagent App
- Example Full Featured Clojure(Script) App
- ClojureScript 30
- Lambda Island - ClojureScript Tutorials
- Purely Functional TV - Clojure(Script) Tutorials
- Getting Clojure
- Elements of Clojure
These resources are great next steps for learning to work with Clojure(Script).