I’ve been playing, of late, with ClojureScript front-ends, specifically with Om and with Reagent. Between the two, I like Reagent much better. The short reason why is that it feels much more ‘Clojurish’ and the programming model feels much more accessible, especially to someone already familiar with Clojure/ClojureScript. Om, by contrast, feels like a thinner wrapper over React And even though it does a number of neat things, it’s ultimately more unwieldy. 1
Reagent and Om are two different libraries which take two different approaches to integrating React into ClojureScript. Of the two, Om is probably the best well known. It was created by the very prolific David Nolan who is one of ClojureScript’s maintainers and this means that Om has a pretty good pedigree. Generally, Om encourages placing the application state in a single
atom. One can then define components by using
reify to implement a protocol. Components can maintain a local state and communication between them is usually managed using asynchronous code, for example using core.async. Components, along with the root atom are passed to the
root function, which mounts the component. The atom is then watched so that changes to it cause components to update parts of the UI. There are a number of extensions for Om, such as om-tools which helps to disguise some of the interface warts and Sablono which allow one to use Hiccup style templates instead of the function calls. There is also an array of pre-made components ready for inclusion in an Om based application.
Reagent, 2 on the other hand, is built around its own version of
atom which uses ‘computed observables’ to keep the UI up to date. Every change to one of these atoms results in an update of the components that depend on it. Components are defined using normal Clojurescript functions. These functions must either return a Hiccup-like data structure or return another function, which in turn returns a Hiccup-like data structure. Those functions are ultimately passed to Reagent which does the hard work of converting them into components. There is less emphasis on local state in Reagent than in Om and the default mode for communication between components is to simply have top level components pass arguments to lower level components. The big difference between Reagent and Om is that while the later primarily exposes React’s API and lifecycle events, the former wraps them with a functional and very Clojurish interface. This means that Reagent feels more natural to an experienced Clojure developer.
Now, Om has some neat ideas baked into it. Using Cursors to allow components to selectively update themselves for example, is pretty cool. But it also has some ugly bits. The basic syntax, for example feels rather complex. Using
reify, protocols, and
#js doesn’t feel very Clojurish. Om-tools helps with this but we’re still left with the underlying problem that defining components in Om is more like implementing an object than writing a function. This has value in that it more directly exposes React’s lifecycle, which, for example allows users to access hooks are mounted for the first time, but it feels wrong. It’s worth noting that while Reagent doesn’t work by default by directly exposing the React’s lifecycle methods, they can still be accessed by defining metadata on components.
Of more concern are special restrictions that surround defining components in Om. The functions which return reified components have to be idempotent. Idempotency is a useful feature, but requiring users to write idempotent functions strongly violates the principle of least surprise. This means that if you want to use local state with an Om component, you can’t simply use a closure to wrap the component in a
let binding. Instead, Om provides it’s own component state semantics. Reagent has the same problem but it resolves it by allowing component functions to return other functions, which can then be wrapped in closures. This feels a lot simpler and more intuitive.
Not that that matters too much, as I’ve found I don’t actually care too much for using local state in components anyway. Om seems to take a strongly opinionated approach that application data should exist in one global atom, which transient state should be component local. However, I’ve found it’s not always obvious to which component state is really local. If you have a table with one item ‘selected’, is the ‘selected’ state part of the table row or the table as a whole? What if you have other UI components that aren’t part of the table at all but whose appearance depend on which row is selected? Allowing components to read the local state of their parent components is a bear and I don’t even want to think about trying to synchronize non-child-parent components. Just throwing that kind of state on a root level atom and using other language features to partition code and data logically seems easier to me. It’s possible that I’ll suffer for my hubris in the future. We shall see.
One final thing is using a Hiccup style data structure to construct components gives Reagent an extra advantage. Generating or modifying Reagent components is just a matter of manipulating this data structure. So, if one wanted to modify the output of a third party library before it was passed to Reagent, you could do so. I don’t think this is possible with Om components which generate React VDOM directly. For this reason, Reagent components are subject to a kind of metaprogramming that Om components are not. This is very cool.
- I haven’t tried the other ClojureScript React wrapper, Quiescent. It looks promising, leaving the question of state management to developer entirely, unlike either Reagent or Om. I’ll have to take a real look at it sometime. ↩
- Formerly known as Cloact; The name was changed on account of the similarity to ‘Cloaca’. A good change, I think ↩