ClojureScript Tutorial
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
- Start a ClojureScript project with shadow-cljs
- Creating a single page application project
- How single page applications work
- Create a shadow-cljs build to manage compilation
- Create a minimal HTML file
- Serve HTML and JS over HTTP
- Write some ClojureScript
- Start shadow-cljs to begin hacking
- Master live coding by understanding loading and reloading
- Add dependencies to use libraries
- Start rendering to the DOM to build a UI
- Create an atom at the top level to hold state
- Use hiccup to add UI controls like buttons
- Make changes to state from event handlers
- Use
defonce
to make global state reloadable - Add a decrement button
- Use the state for more than one number
- Deleting counters
- Eliminating the key attribute warning
- Refactoring to separate components
- Run a release build to make production-ready assets
- Conclusion
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 librariespackage.json
- lists the npm libraries to installpackage-lock.json
- also used by npmshadow-cljs.edn
- the shadow-cljs project configuration filesrc/
- where ClojureScript source code goes
Below src/
are two more directories:
$CMD ls src
main
- where your application code goestest
- 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.
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.
This is a simple example of writing ClojureScript code that runs in the browser. shadow-cljs performed many steps to get this to happen:
- Read and compile ClojureScript code into JavaScript
- Send JavaScript to the browser that is connected to shadow-cljs
- 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:
- An HTML file that contains a reference to the JavaScript
- 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:
:source-paths
indicates where our ClojureScript code is located:dependencies
lists other ClojureScript libraries we need: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.
It will open a window you should see the message that we printed out: "The app has started!"
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:
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.
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.
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.
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:
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.
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 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:
- fast to compile
- many files
- 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:
- slow to compile
- one file
- 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!