Replacing <JSX />
(Part 2/3) - Learning Form Clojure
Today I want to show you what, I believe to be, a better alternative to JSX, it’s called Hiccup, and it’s for Clojure, but don’t panic! no Clojure (or any Lisp) knowledge is required!
I wanted to write a short intro to Clojure here, but then decided against it, because we will only look at a very tiny subset of Clojure .So let’s just dive in and I’ll try my best to explain things as we go :)
(If you’re already familiar with Clojure and Hiccup, I recommend skipping to part 3)
Here’s what HTML can look like in Clojure:
[:ul
[:li "item 1"]
[:li "item 2"]
[:li "item 3"]]
I hope it’s pretty easy to tell what the output will look like, but there are 3 important things to note about this example:
- This notation is called Hiccup,
- This is valid Clojure code, not a DSL.
- This is just data, there are no function calls here ,like JSON.
In Clojure, those things that start with :
are called keywords. A “keyword” in Clojure is kinda like a special string, it evaluates to itself, and as a result, it’s very convenient to use them as keys (or Enums). The []
is a vector, it’s almost like an array is JS. Clojure also supports maps, they are similar to JS objects, and also written with {}
, as we will shortly see.
Hiccup is just a library for working with HTML in Clojure. The cool thing about hiccup is that it allows you use only data(nested vectors) to define your UI tree. When you call the hiccup function on your data it will spit out HTML (or react components).
This approach proved to be extremely useful and eye opening for me, we will explore it very soon, but first let’s look more closely at this hiccup thing.
Hiccup In 1 Code Snippet
Here’s a typical hiccup example, take 2 minutes to read it and compare it with the output below:
[:div.my-site
[:header
[:h1.site-title "Weclome!"]]
[:article {:text-align "left"}
[:h2 "yo!"]
[:p "blah blah..."]]
[:article {:text-align "left"}
[:h2 "maor stuff!"]
[:p "blah blah..."]]
[:footer]]
HTML result:
<div class="my-site">
<header>
<h1 class="site-title">Welcome!</h1>
</header>
<article text-align="left">
<h2>yo!</h2>
<p>blah blah...</p>
</article>
<article text-align="left">
<h2>moar stuff!</h2>
<p>blah blah...</p>
</article>
<footer></footer>
</div>
Basically, we have components as vectors ([]
), that can have N children, if the 2nd param is a map({}
), it’s treated like props, oh and it’s possible to inline class names so they look more similar to CSS.
That’s pretty much it.
It’s Just Data!
You probably heard that everything in LISP is data, and sure, it’s true, but I’m not talking about that.
I’m talking about the fact that this is not a react component:
[MyPanel {:align :vertical} "my stuff"]
It’s just an vector (array) of length 3, we’re not calling React here until the we pass this data to our hiccup function. In contrast to JSX, with JSX we use call .createElement
for each component directly:
// <Panel align="vertical">my stuff</Panel>
React.createElement(MyPanel, {align : "vertical"}, "my stuff")
And because it’s pure data, we can easily manipulate it, something we can’t do with React elements after we called React.createElement()
on them.
Transforming Data
To show you how useful it is to be able to easily manipulate your UI tree, let’s start small with the “className” example from part one.
This example:
// in many projects, I want this:
<div className={classNames({foo: true, bar: true})} />
// to be this:
<div className={foo: true, bar: true} />
This can’t really be solved with higher order components,
we can come close by wrapping (<Div />
), but we will have to do this for each component. So without hijacking React.createElement
I don’t see how it’s possible to inline the call to classNames()
.
But what if instead I gave you a the same data as a recursive JSON file, in the form {element, props, children}, like this:
{
element : "div",
props : {className : classNames({foo: true, bar: true}},
children : [...]
}
And now asked you to run classNames()
on all values
under props.className
(recursively)?
It’s pretty easy to do, right?
function classNameTransform(json) {
let className = classNames(json.props.className);
return Object.assign(json,
{
props : Object.assign(json.props, {className},
children : tree.children.map(classNameTransform)
});
}
All we have to do now is to run classNameTransform()
before we call React:
let hiccupJSON = ({element, props, children}) =>
React.createElement(
element, props,
...children.map(hiccupJSON))
ReactDOM.render(hiccupJSON(classNameTransform(data)));
Basically, we just converted this:
[:div {:className (classNames {:foo true :bar true})}]
To this:
[:div {:className {:foo true :bar true}}]
Everywhere!
I’m shamelessly mixing JS with Clojure because I want to focus on the logic, not a new language, hopefully what we’re doing here is clear, understand that this silly hiccupJSON™ is equivalent to Hiccup, just very ugly, and don’t worry, we will replace it in part-3.
This is actually pretty great! because unlike wrapper components we only need to do this once, at the top level, and it works for primitive elements, and this code is dead simple! simple to write and simple to debug, because it’s just a diff between 2 nested arrays.
Let’s look at another example.
Logic Patterns
Remember the “loading” example from part1?
<Content>
{
if (this.state.loading)
<LoadingMessage />
else if (items.length)
<ItemsList>{items.map(i => <Item key={i.id}>{i}</Item>)}</ItemsList>
else
<ItemsList><ZeroItems /></ItemsList>
}
</Content>
Here’s one way we could write it in Clojure + Hiccup:
[Content
(cond
(@state :loading) [LoadingMessage]
(empty items) [ItemsList [ZeroItems]]
(not-empty items) [ItemsList (for [i items] [Item (select-keys i [:id]) i])])]
Again, don’t worry about it too much..
It’s mainly here so you can compare it with the code example in the next section. In case you wonder about cond
, this is how it works (except that Clojure has lazy sequences):
let cond = (a, b, ...args) => (a ? b : (args.length ? cond(...args) : null) );
// this will return "I'm the chosen one!"
cond(
false, "not me",
true, "I'm the chosen one!",
true, "too late for me")
The important thing to note here is that because we work with basic data structures opens new possibilities for us.
For example, as much as I want, I can’t to do this:
<Content>
{
cond(
(this.state.loading), <LoadingMessage/>,
(items.length === 0), <ItemsList><ZeroItems/></ItemsList>,
(items.length) , <ItemsList>{items.map(i => <Item key={i.id}>{i}</Item>)}</ItemsList>)
}
</Content>
Why? because cond()
is not lazy, and many calls to .createElement()
can be slow!
How slow? this slow:
• array with 2 elements (1 random) vs a single .createElement() call..
✔ array 1,798,125.12 ops/sec ±2.57% (65 runs) fastest
✔ .createElement 284,433.27 ops/sec ±2.19% (60 runs) -84.18%
• .map() over 10 elements, with arrays vs .createElement() calls..
✔ nested arrays 459,924.82 ops/sec ±1.37% (67 runs) fastest
✔ .createElement() calls 15,773.83 ops/sec ±3.01% (62 runs) -96.57%
With this we will have 4 + (items.length) calls to createElement() before cond() is called… that’s bad.. but creating an array and mapping is really fast, so we don’t care too much..
But we’re not done yet, let’s get back to Clojure one last time :)
Extracting Patterns
I want to show you something really cool that can easily be done once we use a data oriented approach.
If we look hard at the previous example, we will notice
that loading
& empty
are 2 very general concepts in many applications, in fact, I just searched the code base of CoolRemote
and found at least 20 uses of “loading” & about 7 uses of “empty” with the exact same pattern,
and 3 more that can be converted to this pattern.
How how did we DRY it?
I created sub-components, composed them, refactored, and recomposed them (because we did it wrong), and refactored again, tested, and wasted tons of time, on things that got nothing to do with adding value to the product.
A smarter me would just extract the logic pattern, like this:
[Content {:placeholder (when (@state :loading) LoadingMessage)}
[ItemsList {:empty ZeroItems} (for [i items] [Item (select-keys i [:id]))]
Here we added 2 “props”, :placeholder
and :empty
. When :placeholder
is not nil
(null)
then we replace the content with the value of :placeholder
prop, same goes for :empty
,
except that the condition is tested against the children (items in that case). To achieve this black magic we can
simply write 2 tiny transform functions.
Using JS with our hiccupJSON™ again:
let mask = ({element, {placeholder, ...props}, children}) => {
element, props,
children: placeholder ? [placeholder] : children.map(mask)
})
let emptyToMask = ({element, {empty, ...props}, children}) => {
element,
props : empty ? Object.assign(props, {placeholder : empty}) : props,
children : json.children.map(emptyToMask)
})
transform = _.compose(mask, emptyToMask)
Notice that React never gets to see the placeholder
and empty
“props”, and like with the first example, nothing is
scattered around our code-base, it’s all in one place, no magic, very easy to understand, test & debug.
Now What?
Getting an ugly 10 line example, that requires compile time, down to a much more readable 2 line, declarative example, is a really big win for me. I can think of many more examples where simple data manipulation can greatly simplify our render() functions, but hopefully, by now you’re convinced that working with data is simple and may have some major benefits over generating code from XML.
Next, we will bring this idea to JS, we will attempt to implement hiccup in javascript, replace our hiccupJSON™, and play around with it, because we can.
(load :comments)load