A new Hiccup compiler for Clojurescript

Summary

In this post I’ll introduce a new hiccup compiler for Clojurescript that:

  1. Never interprets at runtime, thus has zero runtime overhead guaranteed similarly to JSX.
  2. Allows specifying custom tags, predefining tags for fragments and interop with JS React components.
  3. Optimization for production output

Introduction

Javascript has JSX, which looks like HTML:

<div id=”foo”>
<img src={src} />
</div>

This would get transpiled to roughly the following Javascript code:

React.createElement("div", {id: "foo"}, 
React.createElement("img", {src: src}));

A note on terminology: The result of the above expression (i.e. what React.creatElement returns) will be called a “ReactNode”.

Clojurescript doesn’t allow this kind of syntax, but the community came up with Hiccup to define HTML a long time ago (before React was a thing).

It’s syntax is more succinct and very natural to a Clojure developer:

[:div {:id "foo"}
[:img {:src src}]]

You have all the goodies of staying in Clojure syntax such as editing your code with Paredit.

One of the great things about Clojurescript is that we have macros. This means, we can precompile the hiccup syntax to fast React.createElement calls, just like a JSX compiler does. No compiler plugin necessary.

This is exactly what Sablono does. In addition Sablono also ships with an interpreter in case the user forgot to apply the macro to some hiccup.

We can see it in action by calling macroexpand on the html macro of sablono:

(macroexpand-1 '(html [:a {} (foo)]))
=> (js/React.createElement "a" nil (sablono.interpreter/interpret (foo)))

Here (foo) is a function call which could return more hiccup, such as [:span "x"] or it might just return a string or a even a ReactNode. Note, it’s also possible to avoid the interpretation call.

Why not sablono?

Doing anything over and over again at runtime while you could’ve done it at compile time only once is wasteful. This is the main reason why I decided to make a (friendly) fork of it. Other reasons:

  • No support for React fragments
  • No support for creating ReactNodes from React components: (<MyComponent speedy=1><span>hi</span></MyComponent>)
  • No customization support such as adding a little DSL when you’re working on big projects.

Introducing Hicada

Hicada (Hiccup Compiler aus dem Allgäu) is a new hiccup compiler that supports the above and eliminates the runtime interpretation. I.e. if you fail to precompile your hiccup, for instance inside a function:

(html
[:div "List:"
(mapv (fn [x] [:span x]) xs)])

you’ll see a runtime error from React complaining that it doesn’t understand your CLJS data structure ([:span x]). The fix is easy: Add a (html ...) call inside your inner fn.

If you first checkout the main namespace hicada.compiler you’ll probably notice one weird thing: There is no macro for you to use anywhere!

This is intentional: You’re required to create a simple macro and call the function in hicada.compiler with your desired config:

Simple macro

Why this API? IMO, as soon as you use a library at a lot of places in your code (which you would in a typical SPA) you’re constricting yourself unnecessarily:

  • You can’t easily refactor
  • If the library offers a new functionality, it’s hard to “opt-in” or “opt-out” of it globally.
  • You cannot easily opt-in for optimizations in production builds (more on that later)

By wrapping it, you’re much more flexible as your project progresses (I’ll show some real world examples later). You call the library (hicada) only once and your app code calls your own macro throughout your project.

Have you ever noticed how you’ve been able to use React in CLJS (be it Om, Reagent or Rum) and have not had to run any codemod scripts? It’s because the libraries take care of any backwards incompatibilities and provide you with a consistent API.

New functionalities and differences

Let’s now talk about the new features of hicada:


A) If the first argument after the tag is a variable, it’s assumed to be the first child:

;; The following works:
(html [:div child0])
;; This fails:
(let [x {:href "/doc"}] (html [:a x "see docs"]))

I’ve never had the need to dynamically add props to HTML elements so this is why this decision was made. Though, should you for some reason need it, you can use the following syntax:

(html [:> :div props child0 child1])

This will generate:

(js/React.createElement "div" props child0 child1)

B) Support for creating ReactNodes from React class components:

The :> syntax you just saw isn’t actually intended for supporting dynamic props but is for creating ReactNodes from react class components. In JSX you can write:

<Transition in={in_prop} unmountOnExit=true
timeout={{enter: 300, exit: 100}}>
{(state) => ...}
</Transition>

In Hicada you can write:

Interop example

Here you can see: All props were converted to javascript and the kebab-case was converted to camelCase for you.

Note, here the element after the class is always the props, i.e. if you don’t have any you should pass in an empty map {}. This is in contrast to HTML elements since props often get passed around and modified when dealing with react components.


C) Support for React Fragments:

Both, :* and :> are just handlers that you can override in the Hicada config. You can also define your own if you want to. Their definitions are quite simple:

Preexisting handlers

D) Support for custom transformations

This basically let’s you do some transformations just before hicada outputs the JS code. A transformer looks like this:

(defn maybe-transform-something
[[tag props children]]
[tag props children])

I.e. it receives a single vector of three elements: The tag (eg :div), the props (e.g. {:ttip "upload"}) and the children (always a vector). You can then return a new vector of tags, props and children. Note you can also add children or wrap the hiccup in more elements.

Hicada first preprocesses the hiccup, i.e. normalizes all tags like :div.x.y#z to [:div {:class ["x" "y"], :id "z"}]. With the transformers you get a chance to apply more custom transformation before the JS code is emitted.

Best explained by a real world example: On my app I used the material design light library. At the beginning I just added a react lifecycle method which upgrades the just re-rendered react component. This call into the library basically just scanned the DOM subtree given a DOM node and looked for any CSS classes which need some javascript handler attached to it. This worked great… until it didn’t. When I re-rendered a component at the top of the tree with many many child DOM nodes, the upgrading introduced noticeable lag. With Hicada the fix was easy and quick: Detect if the hiccup element uses a dynamic CSS class (they all start with mdl-js-...) and if so, attach a :ref which handles the upgrade of a single DOM node:

Example transformer

And add the transformer to your macro:

Your modified macro

The mdl-ref-handler are incredibly simple:

The ref handlers to upgrade the material components

You can see it in action:

Example

You can see in the output how it attached the ref automatically and even took care of an existing ref function.

This took about 20 minutes and solved all my performance problems.

I also just integrated a dynamic DOM mutating library into React without writing any custom wrappers. This would probably also work with the newer material components web and probably even with many other libraries that simply mutate the DOM. No more waiting on wrapper libraries.


Another example: Personally, I’m a big fan of tool-tips in user interfaces. I use them on almost all buttons. I added a special handler that transforms:

(html
[:button {:ttip "Deletes your input"}
"Delete"])

I won’t show the implementation but it basically just attaches a tooltip (again using refs) to the DOM node. Similarly I added support for inline CSS:

(html
[:div {:css {":hover" {:color "green"}}}
...])

Which generates a CSS class on the fly (a story for another blog post). Media queries, keyframes and automatic prefixing of the generated CSS. All without having dozens of indirections.

Transformer functions really make life simpler in big projects where you use the same patterns over and over and don’t want to use function calls that incur runtime overhead or introduce unnecessary complicated syntax.


E) Support for production performance enhancements.

Check out the code in React.createElement() real quick. Do you see how it takes the arguments you just passed into createElement and copies it to an array? This isn’t done if you pass in a single child argument to createElement. However, you probably know that giving React a CLJS collection (it understands ES6 iterators which all CLJS datastructures implement) or arrays as children will give ugly warnings if you don’t give them a key and you run in development mode of React. This is because in DEV build of React, the result of React.createElement is validated afterwards.

So hicada has a config flag, that allows you switch on this optimization for production. There is no difference in behavior since both return the exact identical ReactNode. The config key is called :array-children?.

Example:

Production array children example

Note cljs.core/array isn’t a function but a macro. It just outputs the JS code [a b], exactly the same as #js[a b] in CLJS. I’m not sure if JSX compiler has this optimization option (it doesn’t look like it). But with this, you’ll avoid copying any arrays unnecessarily. Free speedup.


F) Experimental support for react-native compiling:

(Warning: I have almost no experience in react native)

There are no HTML/string tags in React native, everything is a class. So it makes almost no sense to transform keyword tags (such as :Text) to strings (such as React.createElement("Text", props, child0)). For this there are two options in Hicada:

  • :no-string-tags?: Always outputs a keyword tag, such as :Text as a JS variable Text.
  • :default-ns: If you collect all your class components that you use in project in one single namespace, it will automatically refer to that namespace

Examples:

Example react native usage

In the first example you should have Textdefined in your namespace. In the second you should have something like a (def rn (js/require "react-native")) in your namespace. And in the third example Hicada assumes you have all your (native) components defined in a central namespace.


How to integrate with existing frameworks

An awkward question: How do you use Hicada in an existing code base? If you previously defined you own html macro, the switch is pretty easy. You just call the Hicada function which compiles the hiccup.

Rum will call sablono.compiler/compile-html in its macro. You could add a sablono.compiler namespace (a .clj file) into your project and create a compile-html function which calls Hicada with the config you like. This namespace will take precedence over any CLJ namespace on your classpath.

You can do similarly in Om I suppose.

You can also use hicada with reagent. Reagent will happily work with ReactNodes.

Be aware that, if you relied on Sablono’s interpreter you’ll have to test your components and manually add some (html ...) macro calls. But you’ll see the errors right away in your components.


How to debug issues

It’s really easy to see what hicada outputs. Either you call the hicada/compile in some clojure REPL or call (macroexpand '(html [:div ...])) in you CLJS repl. Where html is the macro you defined.


I hope you find this library useful. It’s on github. Expect some rough edges.