personal website and blog of

Johannes Staffans

Learn to read with ClojureScript, part 2

06.03.2016

Almost a year ago, I wrote about implementing a learning aid for children in elementary school using ClojureScript. I finally got around to finishing this project now and decided to write a few words about the experience.

For the first iteration, I had decided to use Boot, mostly due to the simpler integration of things like SASS compilation into the development workflow. I didn't get very far with the first version of the application until it got put on the back burner, but a few weeks ago, I decided I should really finish the project and save all those children from having to look at my Flash application from 2007.

The project was still on a year-old version of ClojureScript (ancient in ClojureScript terms!) and an old version of Boot. When I tried to fire the app up, nothing worked!

clojure.lang.Compiler$CompilerException: java.lang.RuntimeException:
  No such var: cljs.repl/IParseError, compiling:(cemerick/piggieback.clj:89:1)
  java.lang.RuntimeException: No such var: cljs.repl/IParseError

I tried looking over some more recent Boot/Reagent examples to learn what I should be doing, but couldn't really figure anything out. Giving up, I started a new project from scratch using Leiningen and Figwheel, using the reagent-figwheel template. Oh the joy of greenfields development!

Re-frame

For the new version, I decided to try out re-frame, an Elm-inspired, FRP-ish framework for Reagent applications. Working within the confines of a well-defined framework turned out to be a big win for me. I agree with Malcolm Sparks about the advantage of having a box to put your stuff into when you're getting used to developing in a new environment — for me, in this case, frontend development with Reagent. You should watch that talk, it's pretty good!

Of course, re-frame is no magic bullet. I put a lot of thought into for example what my state model should look like. In the end, I found it best to keep only the things that really change in the app state atom and derive the rest from that. As an example, the application I built has a number of groups of words that the user can select for practice:

  [["att", "den", "ett", " där", "fin", "han" ...]
   ["mitt", "in", "kom", "dem", "mig", "dig" ...]
   ...]

To keep track of the current words to show, I could have put the entire word array into the app state and pushed the currently selected group via a subscription to the view component. But as the actual words constitute just static data, I instead just pushed a :current-group key to the view and let the view component do a lookup of the actual words to show based on that. Re-frame stresses to keep view components dumb; I think a a simple lookup is dumb enough!

Figwheel

I had some problems with Figwheel reloading at first. I used secretary for routing, and whereas the root page reloads fine, reloading while on a route (/#/group/4 for example) would result in a blank page. I could fix that by removing the Fighweel reloading configuration from the Leiningen project file and putting it in a dev namespace instead — it turns out that you shouldn't let Figwheel reset the whole app, just re-mount the root node.

The core namespace has a function that performs initialisation of the app and mounts the root node:

(defn mount-root
  []
  (reagent/render [freq-words-2.views/app] (.getElementById js/document "app")))

(defn ^:export main []
  (dispatch [:initialise-db])
  (routes/init)
  (mount-root))

The project file had this setting:

;; project.clj:

;; reload whole app: causes blank pages when reloading route
:figwheel {:on-jsload "freq-words-2.core/main"}

The replacement for the above:

;; dev.cljs:

;; only re-mount root on reload
(figwheel/watch-and-reload
   :websocket-url "ws://localhost:3449/figwheel-ws"
   :jsload-callback mount-root)

Check out the [sources] 9 to see the whole setup.

Devtools, REPL

Another gem that I discovered during the development of this application was cljs-devtools. It's extremely helpful for logging ClojureScript values to the Chrome console. I enable it in the dev namespace, so it's only available during development:

(devtools/enable-feature! :sanity-hints :dirac)
(devtools/install!)

I didn't use the ClojureScript REPL much — Figwheel reloading and cljs-devtools were enough for a smooth development experience. The rare occasions when I put the REPL to use were mostly for inspecting and interacting with the app state directly.

Conclusion

Leiningen, Figwheel and re-frame brought stability and sanity to my frontend development undertakings. Hats off to all the people who have brought the ClojureScript ecosystem and development experience to where we are now!

You can see the live application here. Sources are on Github.

Back