From Clojure to ClojureScript
》A Journey
How do you get from Clojure to ClojureScript? Though I have had much practice with Clojure and understand the ecosystem around it, understanding the environment of ClojureScript has remained elusive. Surely it is possible to use ClojureScript without having a deep understanding of JavaScript, just like most Clojure code can be written with minimal Java knowledge?
I had dabbled in some JavaScript before — enough to know that I would rather be writing ClojureScript if at all possible. But most of the advice given to me was along the lines of: "go learn JavaScript React and come back later." Almost as if the only people coming to ClojureScript were JavaScript developers!
Here is what I’ve learned as I struggled to put a web app together. I hope it helps other developers who might be coming from a similar background.
》The Destination
The goal was to create something to make entries into Heedy more convenient. Heedy is an aggregator for personal data with some nice analysis capabilities. I have an instance running in my home network and want to be able to collect data for it from my phone even if not connected to my network.
Oh, and I have an Android phone. Supporting iPhone would be nice, but I was not going to wear myself out trying to do that since I have no iDevices for development or testing.
After quite a few false starts on different Clojure-based technology stacks I settled on writing a Progressive Web App. Here are some of those stacks that were considered:
CljFx - There is no route to bundling as a phone app
ClojureDart - If I was going to write a larger and more complex app targeting phones, this might be the way
UIx - Got off to a quick start, but then realized this would require learning React in detail before it could handle my needs
I settled on a fairly bare combo of shadow-cljs
+ re-frame
in the end.
》》What I Learned
Here it is, as short and sweet as I can make it.
》The Browser
My mental model: the browser is a big event engine
The first event is when the browser loads the HTML page you point it to, which in
turn includes inline <SCRIPT>
blocks or <SCRIPT SRC="…">
elements to load
external scripts. These scripts processed as loaded.
Everything else that happens — a page load, a mouse click, a keypress, the response of an HTTP fetch — are all events. Your JavaScript functions are called in response to those events.
Async
Your page only gets one active JavaScript thread. That’s why async calls are so important. Every event you want to handle, like fetching JSON data from an API, should happen in the browser engine, not directly in your code. If you have to do heavy calculation, that should be offloaded to a Web Worker so that your page isn’t frozen while the calculation happens.
Functions like fetch
require passing two functions: one to run on success,
and another to run on failure. The onClick
handlers for UI elements have to be
given a function. Everything is like this. It’s why the land of JavaScript is so
full of .then
. And why using ClojureScript can be so nice.
》shadow-cljs
There are many libraries for creating a Clojure-based HTTP server. But what I needed was a static page plus some JavaScript without a backend API. In a sense Heedy is my backend service, but I am only consuming it’s API.
After testing out a few libraries and stacks, I realized the only way to go was to use Shadow. The Shadow CLJS User’s Guide is a great start, and got me to a point of working with a REPL rather quickly.
A few things to know:
Shadow-cljs provides multiple REPLs. Mainly one for the shadow-cljs process itself, and the one for each build. If you have a
:web
build, then there will be a REPL for the in-browser ClojureScript environment.The build watcher will hot-reload the page any time you save a file.
If you are a VIM fan, you will want to consider NeoVIM. I ran into too many troubles with
vim-fireplace
and found that Conjure is a fantastic upgrade. Totally worth the trouble to switch.Use
:ConjureShadowSelect [build-id]
to connect to the in-build REPL. Yes, the REPL that lives inside the browser!Progressive web apps require being served from a secure context. I had to set up SSL certificates for development and then work out how to get a valid certificate from Let’s Encrypt for production.
》ClojureScript
If you know Clojure already, most of ClojureScript will come very naturally.
Re-frame includes Hiccup for HTML output,
which is very easy to adapt to and sits nicely in your usual functions. Here is
a very simple one that produces a div
with id my-id
and two classes — my-class
and my-other-class
. Inside the div is a p
with some text.
(defn my-component []
[:div#my-id.my-class.my-other-class
[:p "A paragraph with example text"]])
output:
<div id="my-id" class="my-class my-other-class">
<p>A paragraph with example text</p>
</div>
You can place ClojureScript code inside the Hiccup components. Just make sure the code returns valid Hiccup.
interop
The basic ClojureScript interop format is similar to Clojure. Just a little bit was enough to get me started. If you need a nice guide, try this one from 'bits by luke'
You have the full power of JavaScript where you need it.
;Call `.now` on the javascript `Date` object to get a Unix epoch integer:
(.now js/Date)
;Create a new `Date` object and call `.getMonth` on it:
(.getMonth (js/Date.) )
;Get the DOM's title property
(. js/document -title)
;Or some syntax sugar for the same:
(.-title js/document)
;Get nested object properties using ..
(.. js/document -location -href)
》React / Reagent
Despite all the claims that I needed to go off and learn React, there turned out
to be only one thing I really needed to know: how to use an ratom
in a
component. In truth, re-frame does almost all of the dirty work that you would
otherwise have to handle in a React style.
The ratom
works like a normal Clojure atom
. Only it "lives inside" of part
of your page. Imagine you want to have a text field component:
(defn text-field []
(let [text (reagent/atom "")]
[:input {:on-change
#(reset! text (-> % .-target .-value))}]))
The atom is only created once when the page is loaded, but can be referred to in
the component or in any other view you pass it to. In this case every time the
browser fires an onClick
event, the reset!
function is called to update the
value based on the current -value
of the input field.
In React World, you would build up sets of nested components, only sharing the atoms "downward" into children components. That is how you would build up increasingly complex interactions in the page.
But thanks to re-frame there is an easier way.
》re-frame
Re-frame is just magic. This blog post is in no way a substitute for reading the docs. I had tried writing a re-frame based app a few years back, and upon returning to it found that the docs were a tad verbose and jump around in way that makes them hard to use as a reference. This quick start guide from Elia Scotto got me back on track.
Anyway, the magic here is that re-frame uses one big hashmap to store all of your
app state. Then you register subscriptions to subsections of that state hashmap
which can then be subscribed to in components in place of the ratoms
as
described before.
A nice bonus is that the registration step can include modifying the data before it is sent to the view. Imagine you want to sort, correlate, re-arrange, or count some part of the state data before it is shown on the page.
Re-frame makes handling events very easy. In the example above an ratom was
used to track updates to a text input field. By itself that isn’t very useful.
Below is shown how to add an onKeyDown
event that dispatches the ratom’s
content to a re-frame event handler. In this case the :text-submitted
handler.
(defn text-field []
(let [text (reagent/atom "")]
[:input {:on-change
#(reset! text (-> % .-target .-value))
:on-key-down
#(case (.-which %)
13 (re-frame/dispatch
[:text-submitted [@text]])
nil
)}]}]))
》PWA time
The most confusing step for me was making the jump from "just a page" to a working progressive web app. The key bits that go into making it Progressive are:
Using
localStorage
to persist data when the site is reloaded. Re-frame makes this pretty easy and has good examples with their TodoMVC example app.A manifest file which describes the app, provides icons, and generally has what your phone/desktop needs to make the site look like a local app.
A secure context for serving the app.
A Service worker which is necessary to cache your app resources and "serve" them locally.
The MDN docs
were spot on here. You really just need a single .js
file alongside your app
that handles the install
and fetch
event listeners. For basic (but
sufficient!) functionality I pretty much pasted in code from their examples and
included my service-worker.js
file from the main index.html
page.
》The Result
You can find my app on GitHub: HeedyFeedy