published Dec 30, 2016
One of Reagent’s best features is that it enables a reloading-enabled workflow. After opening your app in the browser, you can edit a source file and, after a few seconds, explore the change in the running browser session. Importantly, the app’s state is preserved across reloads. Especially when building UIs, this can be a powerful development tool:
Creators need an immediate connection to what they’re creating. Bret Victor, Inventing on Principle
In general, there are two goals:
Reloading should be reliable. After making a change, you need to be able rely on those changes taking effect. If there are any errors or warnings along the way, you should be notified clearly.
The edit/compile/test cycle should be fast. You want to wait for changes for a few seconds at a maximum. Any delays threaten to break Victor’s Immediate Feedback principle.
Getting started, the first hurdle to overcome is tooling. Often when newcomers experience issues with ClojureScript, tooling problems are the cause. Fortunately, these days it’s relatively straightforward to get started with a project template. Working with reagent, I recommend two projects that have reloading enabled out of the box:
tenzing, based on boot and boot-reload (which displays figwheel-like in-browser warnings)
boot -d seancorfield/boot-new new -t tenzing -a +reagent -n my-test
chestnut, based on leiningen and figwheel
lein new chestnut my-test --reagent
Either of these projects should provide you with a reliable and fast reloading mechanism. But what if things go wrong? Below is a list of the common issues.
You’re making a change to a render function, and yet your web app doesn’t update? The reason may be that React doesn’t get notified of the changes. Reagent rerenders components when:
But if you only change code, neither of those events occur.
Consequently you need to re-render the scene manually. With Reagent, as
with React, this is done simply by re-mounting the root component,
i.e. by re-running r/render
.
With boot and boot-reload, set
the on-jsload property of the reload
task to a function
that re-mounts
the root.
With leiningen and figwheel, you can do the same by simply rendering the component inside the core namespace.
Code changes in your root component are sometimes not picked up after reloading. The solution is simple. If your code looks like this:
defn root []
(:div "Where the magic happens"])
[
root] (.getElementById js/document "container"))) (r/render [
wrap the component in an anonymous function instead:
fn [] [root])
(r/render ("container"))) (.getElementById js/document
If state updates don’t trigger re-renders, one common reason is that you’re accessing the ratom instead of its contents.
This problem is compounded by the fact that get
in
Clojure(Script) doesn’t throw if you pass it an atom. Compare:
cljs.user=> (defonce !state (r/atom {:loading true}))
#'cljs.user/!state
cljs.user=> (get !state :loading)
nil ;; D'OH!
cljs.user=> (get @!state :loading)
true
Call this a design choice, or call it an oversight that cannot be
fixed without introducing a breaking a change. In any case, if state
doesn’t propagate, check every usage of your state atom – may you forgot
the @
symbol.
One typographic convention that can help here is always to prefix variables names for atoms with an exclamation mark:
let [state @!state] ...) (
If you follow this convention, any use of !state
not in
the vicinity of @, deref, swap! or reset! looks suspicious.
Speaking of state, did you check that your !state
is
actually contained in a ratom? Remember, a ratom is like a
clojure.core/atom, except that it automatically remembers whether it was
dereffed in each render function and, if so, marks that function as
requiring a re-render when the ratom is swapped.
I often accidentally write
defonce !state (atom {})) (
and forget to refer reagent.core/atom
as
atom
in the namespace declaration. For this reason, I
prefer explicitly specifying the qualified function name
r/atom
:
ns my-test.core
(:require [reagent.core :as r]))
(
defonce !state (r/atom {})) (
Finally, make sure you’re using defonce, not def, to define the variable. Otherwise, each reload will reset the atom to its initial state.
When returning sequences, be careful to realize any lazy sequences you create, using e.g. map or for.
Sometimes things go wrong. Like any dynamic language, ClojureScript
has Null Pointer Exceptions in the shape of the inimitable truism
undefined in not a function
. Normally, you’ll just fix the
bug, save your file and expect the issue to be fixed in the running
browser window.
Unfortunately, if an exception occurs in a Reagent (or React) render function, React messes up, essentially leaving the entire component tree in a corrupted state. The reasons behind this are ultimately due to issues with JavaScript exception handling, but this behavior is obviously annoying in a reloading-based workflow.
The good news is that the React developers are aware of this issue and have introduced error barriers as a new feature to keep render exception from corrupting the tree. If you want to try this experimental feature introduced in React 0.15 in a Reagent project, try the code in this gist.
This is presumably for side-effects, a blog by Paulus Esterhazy. Don't forget to say hello on twitter or by email