ClojureScript Tutorial

Build your SPA in ClojureScript!

Master Reagent and Re-frame with my ClojureScript Frontend Signature Course.

  • 3 frontend modules
  • 72 detailed lessons
  • 19 hours of video
ClojureScript Frontend: An Eric Normand Signature Course

Want to learn to build a frontend application in ClojureScript using React and Reagent?

In this tutorial:

  • Create a project using shadow-cljs.
  • Manage npm and ClojureScript dependencies.
  • Set up a live coding workflow for faster and easier development.
  • Separate the View from the Interaction when building a UI.

Table of Contents

Introduction

Create a ClojureScript Single Page Application

What is ClojureScript?

ClojureScript is a version of Clojure that compiles to JavaScript. ClojureScript can run in the browser, on Node.js, on Electron, or in mobile apps via React Native. ClojureScript gives you the power of Clojure to exploit the JavaScript library ecosystem.

People use ClojureScript to build:

  • Single Page Web Applications using React
  • Web servers using Node.js
  • Desktop apps via Electron
  • Mobile apps via React Native
  • Serverless Functions (such as AWS Lambda)

In addition, many companies use ClojureScript as a frontend to their Clojure backend. They can share code and data structures between Clojure and ClojureScript.

Why is ClojureScript important?

ClojureScript borrows many ideas from the Clojure philosophy and makes them available in JavaScript contexts. ClojureScript gives you:

  • Functional programming - Immutable data structures and extensive core library
  • Dynamic programming - See code changes instantly and run a REPL in your browser
  • Easy host interoperation - Easily run JavaScript code with no wrappers
  • Access the JavaScript ecosystem - Use any npm library from ClojureScript

ClojureScript was also a very early adopter of React and still strongly prefers it.

Start a ClojureScript project with shadow-cljs

There are many ways to create and manage a ClojureScript project. I recommend shadow-cljs because it is popular, well-maintained, and works the best with npm libraries. We are going to set up a shadow-cljs project from scratch and add Reagent, the most popular React wrapper.

Dependencies

To use shadow-cljs, you will need:

  • Node.js v6.0.0 or higher; the most recent version is preferred
  • npm which comes with node.js
  • Java JDK 8+

Installing Node.js

To install Node.js, go to https://nodejs.org and follow the instructions for installing the LTS version.

Java JDK

To install Java JDK, go to my Clojure install guide, choose your platform, and follow the instructions.

Create the shadow-cljs project with one command

Now that Node.js, npm, and Java are installed, we can create the shadow-cljs project. It's just a single command to create, but we want to be sure we create it in the correct directory. I like to keep my projects in ~/projects, so I will change to that directory first. You should switch to whatever directory you want to keep your projects in:

$CMD cd ~/projects

Then, create the project. I have named it counter-app because I will make a demo counter application in this tutorial:

$CMD npx create-cljs-project counter-app

This command needs some explanation. npx comes with Node.js. It is for executing utility scripts without explicitly installing them. create-cljs-project is the name of the script. It will be downloaded from npm if it has not been already.

The shadow-cljs script will create a new directory and set up the project inside.

$CMD cd counter-app

You can see the structure of the project:

$CMD ls

That will list:

  • node_modules - where npm keeps the installed npm libraries
  • package.json - lists the npm libraries to install
  • package-lock.json - also used by npm
  • shadow-cljs.edn - the shadow-cljs project configuration file
  • src/ - where ClojureScript source code goes

Below src/ are two more directories:

$CMD ls src
  • main - where your application code goes
  • test - where your test code goes

You'll notice that nothing is in those directories for now.

Test your project by running a browser REPL

At this point, you have a working, empty shadow-cljs project. We can test that everything has been set up correctly by running a browser REPL.

$CMD npx shadow-cljs browser-repl

This command will take a few seconds to run the first time.

Here's what it's doing.

We've seen npx. That command runs scripts. shadow-cljs is the name of the script, and it is the one you'll be running to access the features of shadow-cljs that are part of this project. browser-repl tells shadow-cljs to start the browser REPL.

This command requires a bunch of dependencies, which it installs. It then compiles some code, starts a small web server, and opens a browser to it.

When the browser window is open, there will be a prompt in the terminal and a message in the browser.

shadow-cljs browser repl prompt shadow-cljs browser repl message

At the prompt in the terminal, we can now type ClojureScript code to control the browser. Type this:

(js/alert "Hello!")

Then hit enter to execute it.

The browser should show an alert box with the "Hello!" message in it.

shadow-cljs browser repl alert hello

This is a simple example of writing ClojureScript code that runs in the browser. shadow-cljs performed many steps to get this to happen:

  1. Read and compile ClojureScript code into JavaScript
  2. Send JavaScript to the browser that is connected to shadow-cljs
  3. Execute the JavaScript in the browser

This particular ClojureScript code is equivalent to the JavaScript code alert("Hello!"). The parentheses in ClojureScript (like in Clojure) indicate function calls. They surround the function names and the arguments. The function we are calling is js/alert. The js/ part tells the ClojureScript compiler that this is a native JavaScript function and should simply be called by name. Functions in ClojureScript are normal JavaScript functions. "Hello!" in ClojureScript creates a regular JavaScript string.

If you know JavaScript but need to learn Clojure(Script), I suggest reading Learn Clojure in Y minutes and ClojureScript syntax in 15 minutes. If you know Clojure but want to understand the differences with ClojureScript, read Differences from Clojure.

While this is an elementary example of what you can do with the REPL, it does show the complete flow. We will get to more sophisticated REPL interactions later. For now, we can be satisfied that we have tested that shadow-cljs and the ClojureScript compiler are correctly set up.

Creating a single page application project

We've got the browser repl running, but we don't have a real code project. I propose we create a simple application that keeps track of a count. Every time you click the button, the count is incremented. This application will have:

  • State - keeping the count in the browser's memory (no backend)
  • UI in React - a button and a display of the count

In addition, we will have these features during development:

  • REPL - run arbitrary code
  • Live coding - save your code and watch the changes in the browser

I should mention that the live coding is slightly different from what you get in other hot code reloading systems. In my opinion, what we have in ClojureScript is better than in JavaScript. We will see why when we have it up and running.

We will need to set up the project a bit more. Close the browser REPL by hitting Ctrl-C. Then let's get started.

How single page applications work

A single page application is delivered as HTML. The browser renders this HTML into a Document Object Model (DOM). The HTML contains a reference to JavaScript files, which the browser fetches and runs. That JavaScript is responsible for the state of the application and the user interface (UI).

One of the jobs of a UI is to present the state of the application. In browsers this is done by adding, removing, and modifying HTML elements in the DOM. This is called the View. The other job of the UI is to handle interaction events from the user (such as button clicks and typing). Our JavaScript code will convert those interaction events to changes to the state (along with other relevant actions).

We're going to be making one of these. There are two files we need to have ready for the browser:

  1. An HTML file that contains a reference to the JavaScript
  2. A JavaScript file that contains the logic of the application

Let's start with the JavaScript file.

Create a shadow-cljs build to manage compilation

We need to send a JavaScript file, but we are coding in ClojureScript. We need to instruct shadow-cljs to compile the right ClojureScript files into JavaScript and save them to a file that we can serve from our web server. A shadow-cljs build is where we tell it what files to compile and where to save it, along with some other options.

Open up the file shadow-cljs.edn. It should look like this:

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

 :dependencies
 []

 :builds
 {}}

This file is where shadow-cljs is configured for this project. As the extension suggests, it is written in edn, which is very similar to Clojure (and ClojureScript's) literal data syntax. It has three sections right now:

  1. :source-paths indicates where our ClojureScript code is located
  2. :dependencies lists other ClojureScript libraries we need
  3. :builds defines the JavaScript files we want to build

Let's add a new build called :app:

{...
  :builds
  {:app                              ;; name of build
   {:target :browser                 ;; target the browser
    :output-dir "public/app/js"      ;; output js files here
    :asset-path "/app/js"            ;; used to construct the URL
    :modules {:main                  ;; we need one module
               ;; start `counter.app/init` when the JS loads
               {:init-fn counter.app/init}}}}}

We've done quite a lot here. We've told shadow-cljs that we want to build some ClojureScript that we call app that will target the browser. We want the compiled JavaScript files to live in the public/app/js subdirectory. We're putting it there because we will serve everything in public over HTTP (which we will set up shortly). The asset-path tells shadow-cljs what URL prefix to add to URLS to fetch those JavaScript files. That requires some explanation.

:output-dir is a path on the local machine where shadow-cljs will save the JavaScript files it generates. This needs to be unique per build. Otherwise, the different builds would write over each other's files. We don't want that to happen. shadow-cljs will output a lot of JavaScript files during development, not just the one big file. We'll talk about packaging it up as one big file later for deployment.

:asset-path is a URL prefix. Once we're dealing with the browser that is fetching files from an HTTP server, we're not talking about local paths anymore. But notice that they share the last part. When our HTTP server is going, it serves every file out of public/. That means the file public/index.html will be served as the URL http://localhost:8080/index.html, or stated relatively /index.html. public/app/js/main.js will have the relative URL /app/js/main.js. :asset-path tells shadow-cljs what that URL prefix is. In our case, it's /app/js.

Finally, we create a modules section. shadow-cljs can build multiple modules for the same application. For instance, you might have a module for the homepage (which has a small amount of JavaScript) and a module for the main application once you've logged (which has a large amount of JavaScript). Splitting into modules can help serve fewer bytes the your users. We'll just create one module right now. Splitting up modules is an advanced topic.

We create a module called :main. That tells shadow-cljs to create a JavaScript file called main.js inside the output-dir. But what code should go in there? Well, that's what the :init-fn is. We're telling it to include the ClojureScript function counter.app/init. shadow-cljs will do tree shaking which figures out all the code that is needed (and what is not needed) to run that function. That keeps our JavaScript files small. counter.app/init will run when the JavaScript file loads. We can put whatever code we want to start the application.

Create a minimal HTML file

We needed to serve two files: JS and HTML. We've set up the JS, now for the HTML.

For a single page application, it is common to send down a very minimal HTML file. It will load the JavaScript. The JavaScript will look for a div called app and work within that.

Open up public/index.html and add this:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Counter Application</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="/app/js/main.js"></script>
  </body>
</html>

Save it.

Notice that there is a <script> tag that loads the /app/js/main.js JavaScript file. This is the file that we have told shadow-cljs to build from our ClojureScript code. When that file loads and runs, it will start the application.

Serve HTML and JS over HTTP

Shadow-cljs does come with a web server that is easy to configure. It can serve our files during development.

Open shadow-cljs.edn again and add this at the top level of the map. I like to add it just above :builds:

:dev-http {8080 "public"}

It should look like:

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

 :dependencies
 []

 :dev-http {8080 "public"}

 :builds
 {:app
  {:target :browser
   :output-dir "public/app/js"
   :asset-path "/app/js"
   :modules {:main
             {:init-fn counter.app/init}}}}}

That line tells shadow-cljs to start a web server on port 8080 that serves all files in the public/ directory.

Write some ClojureScript

Okay, we're finally ready to write some ClojureScript! Let's just write the minimal possible application. This code will just write to the JavaScript console in the browser.

In our configuration, we told shadow-cljs to run counter.app/init. That means we need to create the counter.app namespace and within that define a function init. counter.app corresponds to the file counter/app.cljs, and it's got to be in the src/main/ directory. So open up src/main/counter/app.cljs and add this code:

(ns counter.app)

(defn init []
  (println "The app has started!"))

Save it.

Start shadow-cljs to begin hacking

Okay, that was a lot of work to get one little browser app working. People have created templates for this setup since it's very standard. But we wanted to learn how everything works. Finally, we are ready to start the build. We want shadow-cljs to watch our build as the files change. So run this command:

$CMD npx shadow-cljs watch app

watch is the shadow-cljs command to watch out source code for changes and automatically recompile them and send them to the browser. app is the name of the build.

Once everything is built (which will take a few seconds), we can open the browser to http://localhost:8080/. That will serve the index.html file we created and load the JavaScript file. Open that in the browser now.

The page is blank (but the title should be "Counter Application"). Open the JavaScript Developer Console. In Chrome, this is accessed through the View -> Developer -> JavaScript Console menu item. There are similar menus in other browsers.

developer console menu

It will open a window you should see the message that we printed out: "The app has started!"

developer javascript console

Master live coding by understanding loading and reloading

Every time the application starts, the init function will be run, and hence that message printed to the console. But we have instructed shadow-cljs to watch our files for changes and rebuild and load any files that change. How does that affect our initialization message? It is important to get a feel for how this mechanism works so that we can be live code proficiently.

Let's play with the file and reloading a bit so we can develop that feel. I like to arrange my browser and editor windows so that I can see both at the same time. Here's what my screen looks like:

browser and editor both visible

Do what feels comfortable for you.

First, let's change the message in the init function. Edit src/main/counter/app.cljs and change the init function's message:

(defn init []
  (println "App initialization!"))

Save the file. We should see a shadow-cljs logo appear at the bottom left of the browser to indicate that it is rebuilding the code. Then some lines appear in the console indicating that the new code was loaded. But we don't see our initialization message printed again. shadow-cljs makes sure that the init function is only run once, when the application loads.

lines in console indicating code was loaded

The application is loaded when the browser loads the page. So hit refresh on the browser to force a page reload.

When the page reloads, we note that there is a line in the console where our new message was printed.

line in console with new message

Now let's add a line of code at the top level and see when it runs.

Add this line just before we define init:

(println "Code reloaded!")

Save the file. Again, shadow-cljs will detect the change, recompile, and send the entire file to the browser.

Look in the console. You should see the "Code reloaded!" message logged there. When shadow-cljs detects a change in a file, it recompiles the file and sends the entire thing to be run in the browser. Anything and everything at the top level will be reloaded. Functions are redefined, variables are re-initialized. Any actions at the top level are run again.

code reloaded message

Try this: add a new line to the end of the ClojureScript file and save it.

shadow-cljs notices that file has changed and reloads it in the browser. A new "Code reloaded!" message appears in the console. All of this is important because it enables live coding. A good understanding of how code is compiled and reloaded is essential to live code effectively. This difference is only important during development. In production, the code does not change like that.

Add dependencies to use libraries

Let's start building our counter application. Remember, it's simple: we want to display a count and be able to increment it with a button. We will be using Reagent, which is the most popular wrapper for the React JavaScript library.

Because we are adding dependencies, we should stop shadow-cljs. Open the terminal and hit Ctrl-C.

Then install React using npm:

$CMD npm install react react-dom

This adds react and react-dom, the two main React libraries. npm will download them and install them under the node_modules/ directory. It will also add them to the package.json file. Open it and look for the "dependencies" key.

{
  "name": "counter-app",
  "version": "0.0.1",
  "private": true,
  "devDependencies": {
    "shadow-cljs": "2.15.2"
  },
  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
}

That installed React, which is a JavaScript library hosted on the npm repository. Now we need to install Reagent, which is a ClojureScript library managed by shadow-cljs. Open up shadow-cljs.edn. It looks like this:

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

 :dependencies
 []

 :dev-http {8080 "public"}

 :builds
 {:app
  {:target :browser
   :output-dir "public/app/js"
   :asset-path "/app/js"
   :modules {:main
             {:init-fn counter.app/init}}}}}

Add this line to the :dependencies vector:

[reagent "1.1.0"]

The file should now look like this:

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

 :dependencies
 [[reagent "1.1.0"]]

 :dev-http {8080 "public"}

 :builds
 {:app
  {:target :browser
   :output-dir "public/app/js"
   :asset-path "/app/js"
   :modules {:main
             {:init-fn counter.app/init}}}}}

Save the file. Start the watcher again:

$CMD npx shadow-cljs watch app

It will read the new dependencies, download them, and install them. Then it runs the watcher as before. Reload the browser to reconnect it.

NOTE: I have carefully chosen and tested these instructions with these particular versions of React, React-DOM, and Reagent. Please use these versions, even if a new version has come out since. I try to keep things up to date, but releases can come suddenly and testing takes time.

Start rendering to the DOM to build a UI

Okay, let's start building our UI. Remember, the general schema for a React UI looks like this:

React UI Schema

In our specific case, we will store the current count in the state. Our View will show the current count and a button. And clicking that button will change the current state (which will re-render).

Let's create our View first. We write a new component. It's just a placeholder for now.

(defn Application []
  "Hello!")

In Reagent, components are defined as regular ClojureScript functions that return Hiccup. Hiccup is a de facto standard for representing HTML using Clojure data structures. You can learn more about Hiccup in my ClojureScript Reagent Guide.

We want to develop interactively, which means we don't want to write too much code before we see it working. This is just enough of a component to see it do something.

Now we need to render this component into the DOM element with id app that we created in the HTML page. Our application will control everything inside that DOM element.

To do that, we need to add Reagent's namespaces:

(ns counter.app
  (:require [reagent.core :as r])
  (:require [reagent.dom  :as dom]))

Then we can run this code:

(dom/render [Application] (js/document.getElementById "app"))

Place that just after the Application component is defined at the top level. Remember, this means that it will run every time the file changes. Every time we modify this file, the entire application's React component tree will be rendered to the DOM. This will work well for us here. But you may want more control, in which case you should look at the shadow-cljs lifecycle hooks.

Save the file and you should find that we now have an excited greeting in the browser window.

This message, in fact, is too excited. Let's change the Application component to have a period instead of an exclamation mark.

(defn Application []
  "Hello.")

Save the file and watch as the browser changes as well. This is a simple example of live coding. We will get more sophisticated shortly.

Create an atom at the top level to hold state

Let's create the state that will hold the current count. Add this at the top level, after our println message but before Application.

(def current-count (r/atom 0))

Save it. This will create a global variable called current-count that points to a Reagent atom. Reagent has its own implementation of atoms (different from the built-in ClojureScript atoms). They work the same except that keep track of which components read them. That lets the atom re-render just those components that need to when the atom changes.

Let's see that in action. Change the component to this:

(defn Application []
  @current-count)

After you save, you should see the Hello. message change to 0, the current count. The @ sign is dereferencing the atom, meaning it is reading its current value. Dereferencing a Reagent atom inside of a component lets Reagent know that the component needs to be re-rendered whenever that atom changes.

Use hiccup to add UI controls like buttons

Our view now shows the current count, but we don't have a button. Let's add it. We're going to write a bit of Hiccup. Structurally, it's just like HTML. Each element has a tag name, attributes, and children. Only the syntax is different. Let's wrap the count in a <div>.

(defn Application []
  [:div @current-count])

That's all you need to wrap the count (which is a number) in a <div> element. You can't see the change in the browser unless you inspect the DOM elements.

Now let's add a button next to the count:

(defn Application []
  [:div @current-count [:button "+1"]])

Save it and you should see the count and the button. There's no styling, but it's there. And also note that we didn't have to reload the browser or wait very long for the change to show in the browser.

Make changes to state from event handlers

We have a button. We want it to increment the state. Let's make it do that.

We're going to add an onClick handler to the button. In HTML, that's an attribute. In Hiccup, the attributes go in a map just after the element name. Modify the component to add the onClick handler like this:

(defn Application []
  [:div @current-count [:button
                         {:onClick (fn [] (swap! current-count inc))}
                         "+1"]])

You won't see a change yet. But once the new code loads, you can click the button and watch the number go up. Click it a few times. Yay! We can count!

Remember: We have not had to reload the browser since we added the Reagent dependencies. This is live coding. Let's make a small stylistic change.

I don't like how the count is right next to the button. There should be a space between them. Adding a space is easy.

(defn Application []
  [:div @current-count
   " "
   [:button
    {:onClick (fn [] (swap! current-count inc))}
    "+1"]])

Save it and now we see a space. However, the count went back to zero! That's not good. When we're live coding, we don't want style changes to change the state. We want to put the application into a particular state and style it at that state. We don't want to click around again to make sure it looks right with numbers higher than zero. We can fix that.

Use defonce to make global state reloadable

We don't want to lose state every time the code reloads. We can use a built-in feature of ClojureScript to fix it.

At the moment, we're storing our state in a global variable defined with def:

(def current-count (r/atom 0))

Because it is at the top level, that def will run every time the code reloads. def does not discriminate between an existing variable and a new variable. It will set either one. In this case, our def is creating a new r/atom initialized to 0 each time the code reloads.

What we want is a way to define variables that only sets a new variable and leaves existing ones alone. That's how defonce works.

(defonce current-count (r/atom 0))

defonce checks whether the variable already exists. If it does, defonce does nothing. If it doesn't, then it creates a variable and initializes it. The first time it loads, the variable doesn't exist, so defonce creates it. But every reload after that, nothing happens. That means we keep our state, even when all of the code reloads.

Let's try it out. Save this file (with the defonce) and see the result in the browser. Now, click the button a few times to increment the count. Now let's force a code change. Let's add a header to the component:

(defn Application []
  [:div
   [:h1 "Counter"]
   @current-count

Watch as the UI reloads automatically. The count should not get reset back to 0! This behavior is what makes ClojureScript live coding superior to other systems. You don't lose the state as you change the code.

Add a decrement button

We can count up, but not down. Let's add another button.

   @current-count
   " "
   [:button
    {:onClick (fn [] (swap! current-count inc))}
    "+1"]
   [:button
    {:onClick (fn [] (swap! current-count dec))}
    "-1"]]])

The code should reload and the counter should stay. Now click the -1 button. It should go down!

Use the state for more than one number

We are currently storing just one number in the state of our application. Let's make it so that we can have many counters.

Let's make a new variable called counters:

(defonce counters (r/atom []))

We want a collection of counters (0 or more) and we want them to stay in order. We will also like to access them by index and append to the end. So a vector will work nicely.

Save.

We want to display all of these counters. So let's modify our Application component to loop through the counters and generate HTML for each.

   [:h1 "Counter"]
   (for [counter @counters]
     [:div
      @current-count
      " "
      [:button
       {:onClick (fn [] (swap! current-count inc))}
       "+1"]
      [:button
       {:onClick (fn [] (swap! current-count dec))}
       "-1"]])]])

That's not quite right, but it's a step in the right direction. This shouldn't show any counters at the moment since that vector is empty. Let's make a button that adds a new counter:

       "-1"]])
   [:button {:onClick (fn [] (swap! counters conj 0))} "Add counter"]])

Save it and click the new button. We should see a counter. Click it again. A new counter.

There are two problems right now:

If you check the console, you'll see a warning about lazy sequences inside of Hiccup.

Lazy sequence warning message

We also are still displaying the original counter everywhere.

Let's address the first problem since it is easiest.

Laziness does not work well with Reagent's model of rendering. Reagent needs to know which component is accessing an atom in order to know which component should re-render when that atom changes value. But since for is lazy, the code doesn't execute until after the component returns. There is no way to know which component generated that lazy sequence. The answer is that you have to force the sequence to evaluate strictly before the function returns. Just wrap it in a doall:

   [:h1 "Counter"]
   (doall
    (for [counter @counters]

doall forces a lazy sequence to be fully realized immediately. Now the warning should not appear anymore.

The second problem was that we are accessing current-count inside of the for, when we should be accessing the counter local variable. Let's change that.

    (for [counter @counters]
      [:div
       counter
       " "

That's great. But our buttons are still modifying that current-count atom. We need to change the click handlers as well. The question is, how do they know which count to change? Because our counters are in a vector, we will need to provide the index. Let's add that to the for.

   (doall
    (for [[i counter] (map vector (range) @counters)]
      [:div
       counter

That's a little trick I've picked up through the years. We don't have a REPL connected, but we can run things at the top level when we save:

(println (map vector (range) [:a :b :c :d]))

It should print this to the console:

([0 :a] [1 :b] [2 :c] [3 :d])

It's a way of pairing an ordinal index with the values in a collection.

Now, one more step: Let's make the increment/decrement buttons work.

       [:button
        {:onClick (fn [] (swap! counters update i inc))}
        "+1"]
       [:button
        {:onClick (fn [] (swap! counters update i dec))}
        "-1"]]))

Save it and try it out. You should be able to add counters and increment/decrement them all independently.

Deleting counters

Once we add a counter, we can't remove it. Let's add buttons to each counter so that we can remove it.

First, add the button:

       [:button
        {:onClick (fn [] (swap! counters update i dec))}
        "-1"]
       [:button
        {:onClick (fn [] )}
        "X"]]))

The button doesn't do anything. We need to delete an element from the vector, given its index. But Clojure doesn't have anything like that. We'll have to write it. Add this above the component:

(defn vec-delete [v i]
  (into (subvec v 0 i) (subvec v (inc i))))

Let's test it. We don't have a REPL connected, so add this to the top level and check the console:

(println (vec-delete [:a :b :c] 1))

The answer [:a :c] should print to the console since it should remove the element with index 1. You can delete that line from the top level.

Now that we have a function to delete an element from a vector, let's use it in the click handler for our delete button:

       [:button
        {:onClick (fn [] (swap! counters vec-delete i))}
        "X"]]))

Save it, watch it refresh, and try the button out. Does it work? :)

Eliminating the key attribute warning

If you check the console, you'll see that React is complaining about some elements in an array not having key attributes.

React key warning

React expects that when you build lists of components dynamically, they should have a way to identify themselves. That helps when the list changes a lot so that React can figure out which ones are moving, which ones are new, and which ones are deleted. It's good practice to get rid of that warning. We can do so easily. Add a :key attribute to the [:div] immediately inside of the for comprehension:

    (for [[i counter] (map vector (range) @counters)]
      [:div {:key (str i)}
       counter

React has specific requirements. The key should be a string. It should be unique to that list. And it should be stable. The index itself will make a good key.

Save it and see that the warning doesn't return.

Refactoring to separate components

Our entire UI is inside of one component. While it's not big, it's important to see how we can move some of the Hiccup into its own component.

I think the [:div] inside the for comprehension is a good candidate for making its own component. Each of those represents a counter. Let's move that code into its own component.

The Hiccup inside there depends on two things: the counter and the index. Those will be the arguments to the component:

(defn Counter [i counter])

Save.

Now we can move the markup into this new component:

(defn Counter [i counter]
  [:div {:key (str i)}
   counter
   " "
   [:button
    {:onClick (fn [] (swap! counters update i inc))}
    "+1"]
   [:button
    {:onClick (fn [] (swap! counters update i dec))}
    "-1"]
   [:button
    {:onClick (fn [] (swap! counters vec-delete i))}
    "X"]])

(defn Application []
  [:div
   [:h1 "Counter"]
   (doall
    (for [[i counter] (map vector (range) @counters)]
      [Counter i counter]))
   [:button {:onClick (fn [] (swap! counters conj 0))} "Add counter"]])

That should work, but the :key warning shows up again. The trouble here is that we are putting the :key attribute, but on the wrong element. It should go on the component that is immediately inside of the list, not one level deeper. We can fix that with a different way of setting the key: using metadata.

First, we remove the :key attribute inside of Counter

(defn Counter [i counter]
  [:div
   counter

Then we add :key metadata to the Counter component inside of Application.

(for [[i counter] (map vector (range) @counters)]
  ^{:key (str i)} [Counter i counter]))
[:button {:onClick (fn [] (swap! counters conj 0))} "Add counter"]])

And that should do it!

Run a release build to make production-ready assets

We've been running development builds using shadow-cljs this whole time. Development builds are:

  1. fast to compile
  2. many files
  3. bulky files

Development builds are that way to optimize for developer experience. You want them to be fast and in many files so you can see the changes quickly. And you don't mind that they're bulky because they're loaded on your local machine.

However, in production, everything is reversed. Production builds should be:

  1. slow to compile
  2. one file
  3. slim code

Production builds should be slim so that the client has less to download. They can happen over the entire program and slowly because it's only done once just before release.

First, lets kill the shadow-cljs watcher. Then, we do a release build with this command:

$CMD npx shadow-cljs release app

release is the command and app is the name of the build. This will overwrite our development build with the release build. That way, we can still run it the same way: using an HTTP server.

After building the release asset, we can run a server like this:

$CMD npx shadow-cljs server app

With that running, we can reload the browser and try out the app as a production user will experience it.

It's a good idea to test it out because it does go through a set of advanced optimizations. Those optimizations have a very small chance of changing the behavior. Most of the time it just makes the code load and run faster. But it's not unheard of that the optimizations are too agressive. So give it a test.

If you're ready to deploy to production, you can include public/index.html and public/app/js/main.js.

Conclusion

In this tutorial we've set up shadow-cljs, configured it for a live coding workflow, and set to work building a simple app. We refactored the app a bit and then built a release version of the app for production. Pretty handy work!

Build your SPA in ClojureScript!

Master Reagent and Re-frame with my ClojureScript Frontend Signature Course.

  • 3 frontend modules
  • 72 detailed lessons
  • 19 hours of video
ClojureScript Frontend: An Eric Normand Signature Course