Students of the Game: Reloadable Code
About two years ago I built a small ClojureScript app that looks like this:
This app was meant to be a bite sized learning project to level up my ClojureScript interop skills.
Aside from being gorgeoous, it had only two things to do:
- User presses the
Calendar Eventis created
- User can see a
I started the app as I always do which includes running
Hot Module Reloading (HMR). As I'm jamming away on the code I started to notice some interesting behaviour.
When I pressed the
Add button, instead of
Calendar Event being added,
5 were added! I paused for a moment before hard refreshing the browser. Then I clicked the
Add button again. Unlike the first time, everything worked as expected; only
Calendar Event was added. I smiled to myself and marvelled at my incredible detective skills: obviously the problem was
Since I obviously identified the source of the problem I decided to resolve the issue by ignoring it. So any time I made a change which triggered an
HMR reload I would hard refresh the browser like a savage.
Some time later I realized that, no, the problem was not
HMR...it was my code. As it turns out,
HMR isn't free. It requires the code author to design and build the code to be reloadable code.
So in the intrepid spirit of craftsmanship, I have resurected my Calendar App and refactored it into
reloadable code. What follows is the process one could take to achieve
- Intro to Hot Module Reloading
- "Submit" event listeners
- "Change" event listeners
- Populating Dropdown Options
- Managing App State
HMR is when we have a special program running outside of our app. The job of this program is to watch for changes to our
.cljs files. When our special program detects changes it will tell the browser that there are changes in our code, send those changes to the browser, and trigger a "reload" which means our running app is automatically updated with the new code and the app state is exactly where we left it.
The reason we do this is because:
- We don't like manually refreshing our browsers.
- We don't like losing our applications state
- We despise suffering through slow development feedback loops
As I mentioned earlier, just because you use
shadow-cljs and the HMR mechanism they provide, does not mean you can take advantage of all the powers of HMR. You first have to architect your code in a particular way. Specifically, you have to control your side effects. In other words, you have to write
Okay, now that we have reviewed what HMR is doing, lets dive into the code and transform it into
If you save
calendar.cljs 5 times and then press the
Add button in the Calendar App you will see it creates 5
If we inspect the console we can see that there are actually 5 event listeners attached to the
submit event. So it seems that when we click
Add 5 event handlers are fired one after the other.
Why is this happening? As it turns out, if you look to the code here:
(events/listen (.. js/document (querySelector ".calendar-form")) "submit" handle-add-event!)
The code above is invoked in our
ns meaning that it runs everytime a
reload is triggered. In other words, if you trigger 5
reloads it would be as if you wrote this:
(events/listen element "submit" handle-add-event!) (events/listen element "submit" handle-add-event!) (events/listen element "submit" handle-add-event!) (events/listen element "submit" handle-add-event!) (events/listen element "submit" handle-add-event!)
Wait. Didn't we say that each
reload triggers a browser refresh? Not exactly. What we mean is that the ClojureScript in your app is sent to the DOM and re-run. The effect makes it appear like the browser is refreshing. Thus, triggering a
reload updates your ClojureScript and re-runs your ClojureScript, but unlike your ClojureScript, whatever you did the DOM previously prior to a
reload stays done.
This is why when we write reloadable code we have to control what we do to the DOM so we don't cause unexpected behaviours like 5
Calendar Events added at a time.
So how do we control what we do to the DOM to avoid these unpredictable side effects? We write
reloadable code and to do this we have to know two things:
- when is our code going to reload?
- when is our code going to finish reloading?
figwheel, as well as other programs, provides us with a mechanism so we can answer the above 2 questions and that mechanism looks like this:
(defn ^:before-load teardown  ; ...do stuff ) (defn ^:after-load setup  ; ...do stuff )
Before we go further, lets breakdown what the above is doing.
setup are nothing more than clojure functions. The only part we have to care about from the above are the
^:after-load words that come before
^:after-load are called metadata in Clojure(Script). What makes them
^ that comes before the
When figwheel sees these particular pieces of metdata infront of a function it knows that it has to run our functions before and after the CLJS reloads.
Now that we know we can write code that runs before and after load we just have to figure out what we should put in these functions to make our code
Going back to the "submit"
event listener scenario, the problem is that everytime our code reloads, a new
event listener is attached to the "submit" event. But what we want is to only have 1
event listener, the newest
event listener, attached to our submit. So what we need to make our code do:
- before reload: remove old "submit" event listener
- after reload: add new "submit" event listener
The following is what the above looks like in our code:
; before reload: remove old "submit" event listener (defn ^:before-load teardown  (events/removeAll (.querySelector js/document ".calendar-form"))) ; after reload: add new "submit" event listener (defn ^:after-load setup  (events/listen (.. js/document (querySelector ".calendar-form")) "submit" handle-add-event!)) (defonce initial-load (setup))
What we did:
before-load- figwheel is going to call our
teardownfunction which deletes event listeners from
after-load- figwheel is going to call our
setupfunction which adds event listeners to
You might have also noticed that I snuck in a little something extra:
initial-load. Remember how we said that
before-load happens before a reload and
after-load happens after a reload? This means those are only triggered when a reload is triggered. If we left it like that, our code would not work when we first visit the app. So we add a
defonce so that the code which runs on the first, and only the first time, your app loads in the browser.
With the above in place go ahead and try to trigger some reloads. If everything worked, no matter how many time you trigger a reload only 1
Calendar Event should ever be created.
If you are lost at this point for any reason take comfort in the fact that writing
reloadable code is not always straighforward and does require you to think deeply about what your code is doing. This is a skill that can take some time to learn.
Here is the second opportunity for making our code
reloadable code. Similar to the "submit" example above, we are invoking another
event listener in our file for the
(events/listen (.. js/document (querySelector "#event_start")) "change" update-event-end-dropdown!)
If you read the code, or even ran the code, you will see that this code is not a problem. Yes, if we
reload our app 5 times it would do this:
(events/listen element "change" update-event-end-dropdown!) (events/listen element "change" update-event-end-dropdown!) (events/listen element "change" update-event-end-dropdown!) (events/listen element "change" update-event-end-dropdown!) (events/listen element "change" update-event-end-dropdown!)
But it's still not causing any bugs. The reason is because the code inside of
update-event-end-dropdown! is destructive and not performing an additive effect like "submit". The worst that happens is that if we
reload 5 times the
option will get set 5 times because of
So if this is not a bug, then why talk about it? Because even if the code is not causing problems now, it is stacking
event listeners and if we change
update-event-end-dropdown! to accidentally do something different, we are going to set ourselves up nicely for an interesting bug.
If neither of these arguments persuade you, that is fine and highlights the interesting part about writing
reloadable code: many elements of designing your code in this way are going to be subjective and based on how you want your code to run.
Having said this, for this scenario I am going to play it safe and show you how we could fix the code:
(defn ^:after-load setup  (events/listen (.. js/document (querySelector ".calendar-form")) "submit" handle-add-event!) (events/listen (.. js/document (querySelector "#event_start")) "change" update-event-end-dropdown!))
You can see the above change here. In the next section we are going to explore more subjective goodness.
Reading along with the code in Calendar App, we have another invokation when the
(set! (.. start-time-dropdown -innerHTML) (time-option-list (time-range))) (set! (.. end-time-dropdown -innerHTML) (time-option-list (time-range 9.25)))
All these do is populate the time options in our forms time dropdowns. So the question is, if we run this multiple times in a row what happens? Similar to the "change" event listeners, the above is performing a destructive action. each time they are run they will delete the content of the
select boxes and replace them with the same options. Also, unlike
event listeners these do not stack. Will there be a bug? What are the problems with this?
Each time we run this code, it clears out our dropdowns and re-adds options. What if we put this in
initial-load? It would only run once meaning that if we made changes to
time-option-list we would not see the effect take place. But what if we know we are 100% done with
time-option-list? Perhaps we don't believe their will be more changes? Maybe this justifies only putting it into the
However, if you are in the camp that believe we should re-run this action just to be safe, then we can put it into the
after-load. But if we do this there is something that could happen. Imagine this is what I did:
- select a
- realize I need to make a change to the code
- change the code + trigger a
If we move the
time-option-list inside the
after-load everytime a
reload is triggered we are going to lose our
end time we selected. We are losing app state. If this is concerning to you, then it is not enough to add our
time-option-list to the
after-load we also have to add additional code to our app to potentially save the state of the application so we don't lose it. I won't walk through what that looks like, as I wanted to bring it up as a lead to what makes
HMR feel like magic: app state.
App State is a memory of what you did in the app. When we trigger a reload we can make it so our add does not reset its app state. For example, let's pretend I am working on my add, I add a few
Calendar Events and trigger a reload. Those
Calendar Events I just added will be lost. We go back to 0. This does not have to be the case though. We could tell our add to not reset the state.
To make this happen all you have to do, based on how I wrote Calendar App, is this:
; update this line (def app-state (atom )) ; to look like this (defonce app-state (atom ))
That's it. Now, whenever your app
reloads, your state is never forgotten. The way this works is explained nicely in Hot Reload in ClojureScript. The short answer:
defonce means that
app-state will not reset to
(atom ) when the app
This is a simple example, but it should provide some ideas for what needs to be done to make our
reloadable code capable of managing state.
reloadable code can seem tricky at first, but hopefully we can see that with ClojureScript it is an achievable and worthwhile goal. For me the benefits of writing your code like this and even thinking about these things is the understanding you gain about how your code works.
The other gain is the power that ClojureScript gives you out of the box. It take very little to setup HMR and start writing reloadable code with the tools provided by Clojure(Script) core library. You don't need additional dependencies.
Hopefully this provides a decent example set with which to start your reloadable code journey.