Liking cljdoc? Tell your friends :D

title: Reloadable Code layout: docs category: docs order: 26

Writing reloadable code

Figwheel relies on having files that can be reloaded.

Reloading works beautifully on referentially transparent code and code that only defines behavior without bundling state with the behavior.

Let's face it though, we are in JavaScript's domain and mutating state is just the way stuff gets done.

Load Time Side-Effects

Load time side-effects are the side-effects that change the state of our JavaScript environment when a file is loaded. As an illustration: when a file with code in it is loaded, it will be changing the state by importing that code into the environment. That is a side-effect of loading the file but it is exactly the side-effect we want when we load a file.

If you are mutating the DOM or your program state at the top-level of your file so that these state changes will occur when the file is loaded, we are going to have to put some thought into how we can safely make these things happen so that we can protect the state of our running program.

Reloadable code is code where load time side-effects will not interact with our running program in a destructive way.

The good news:

  • it is idiomatic in ClojureScript to write our functions so that they don't exhibit bad load time side-effects
  • ReactJS makes it trivial to work with the DOM in a declarative way that is very reloadable

The upshot is: if you are using React or one of the ClojureScript libraries that wraps it, it's not hard to write reloadable code, in fact you might be doing it already.

Potential Problems

There are several coding patterns to look out for when writing reloadable code.

Top-level mutable state

One problematic pattern is top-level definitions that have local state.

(def state (atom {}))

The state definition above is holding program state in an atom. Every time this definition gets reloaded, the definition will be redefined and reset back to the empty map {}. This is normally not desirable.

The way to fix this is to use defonce

(defonce state (atom {}))

This will fix most situations where you have a top-level definition. It is important to remember that if you change code in a defonce you won't see the changes after a reload because it won't be evaluated after the first load.

Page initialization

It is not uncommon to initialize and launch your program from the top-level of a file. However, you normally won't want to run this initialization every time this file gets reloaded.

We can solve this problem by using defonce again. For example:

(defonce initialize-block
  (do
     (initialize-dom)
     (add-body-listeners)
	 true))

Above, we use the name initialize-block but you can use any name because it will probably never be referenced in your program. We use a do block because defonce can only take one form besides the name. At the end of the do block we provide a value true so that the defonce is only invoked the first time it is evaluated. If add-body-listeners returns a truthy value we don't have to provide the true at the end.

Running your initialization code the first time the page loads can also be accomplished by registering a load handler on window.

(ns example.core
  (:require
    [goog.events :as events]))

(events/listen js/window "load"
               (fn [_]
                 (initialize-dom)
                 (add-body-listeners)))

Setup and teardown pattern

If you are working on a page where you are mainly altering the DOM directly but you still want to use hot reloading, you can normally manage almost any complex state with a setup and teardown pattern.

Create a couple of functions named setup and teardown to help reset your application on every reload.

A trivial example is to add a listener in setup and remove it in teardown.

(ns ^:figwheel-hooks example.core
  (:require
    [goog.dom :as dom]
    [goog.events :as events]))

(defn ^:after-load setup []
  (events/listen
   (dom/getElementByTagNameAndClass "a" "submit-btn")
   "click"
   (fn [e] (handle-submit)))))

(defn ^:before-load teardown []
  (events/removeAll
   (dom/getElementByTagNameAndClass "a" "submit-btn"))

;; and we'll want to call setup on the first initial load as well.
(defonce init-block (setup))

The above is taking advantage of Figwheel's metadata markers to specify functions to call before and after a reload. The ^:figwheel-hooks marker is required to let Figwheel know that there are reload hooks in the namespace.

Use ReactJS

When you use ReactJS, state is mediated and managed for you. Your code describes what should be there and then React takes care of all the state changes needed to bring the DOM inline with your description. For this reason React is a prime candidate for writing reloadable code.

Conclusion

There are many ways to make your code reloadable. Once you start paying attention to load time side-effects, the exercise of writing reloadable code becomes fairly straight forward.

Since a great deal of programming complexity stems from complex interactions (side-effecting events) between things that have local state, it is my belief that being more attentive to load time side-effects, leads developers to reduce the amount of state and stateful objects that are strewn throughout their programs and take a more intentional approach to where state is stored and how it is transformed.

Reloadable code is often simply better code.

Can you improve this documentation? These fine people already did:
Bruce Hauman, Michael Camilleri, Alan Thompson, Jindrich Mynarz, Jonathon McKitrick & Thiru Thirunavukarasu
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close