Replacing <JSX />
(Part 3/3) - Using JavaScript
In part-2 we looked at how we can use simple data to represent our entire UI tree. We also converted 10 lines of ugly imperative JSX code, into 2 declarative lines, simply by extracting patterns from data, and writing a function.
Today it’s time to bring the power of data to JavaScript tam! dam! dam!
So let’s not waste time, and create us a hiccup function.
Hiccup in Plain JS
Really the only missing part for using Hiccup in JS is this label thing that hiccup uses for primitive components (:header), but we can just emulate it with strings.
Like this:
[":ul.my-list"
[":li", [":b", 42]]
[":li", [Item 42]]
[":li", {}, "just text"]]
I kept the “:” prefix to avoid possible (but unlikely) collisions simple string, and it helps me to better see what is what, but feel free to use a different prefix or drop it altogether.
The core idea is very easy to implement, all we need is a little recursive function that will apply .createElement
on arrays.
Basically, we want to Convert this:
[":div", {className : "foo" }, "child"]
To this:
React.createElement("div", {className: "foo" }, "child");
Very similar to what we already did in our hiccupJSON™, the only difference is that we use arrays now.
This is all we need:
function hiccupFactory(React, transform) {
transform = (transform || _.identity);
return function H(fragment) {
let [element, props] = [null, {}];
if (!_(fragment).isArray())
return fragment;
if(fragment.length > 1 && !_(fragment[1]).isPlainObject())
[element, ...children] = fragment;
else
[element, props, ...children] = fragment;
// our transform is built-in now:
[element,
props,
...children] = transform(element,
props,
children);
if (!element)
return null;
if(_(element).isString()) // remove the ":" prefix
element = element.split(":")[1];
return React.createElement(
element,
props,
children.map(H));
}
}
I hope this code is pretty clear, I used a bit of lodash to make it more readable.
The hiccupFactory
function is only there so that we don’t need to .bind
the params. don’t worry that this code is not optimized and doesn’t deal with some edge-cases, that’s okay, I just wanted to show you the meat.
Notice that we pass it a transform
function, in contrast to hiccupJSON™ where we wrapped the whole thing with a transform function, this just makes it easier to write transform functions (because we don’t need to worry about recursion), we can of course still wrap H()
anytime.
This how we can use it:
const H = hiccupFactory(React);
let UseHiccup = () => H(
[":article",
[":h2", "I Use Hiccup In JS"],
[":p", "it feels great really..."]])
Extending Hiccup With… Hiccup!
Some of you probably noticed, that your hiccup function lacks the support for inline class names (`:div.my-class
),
that’s ok, we can add it as a “plugin” (with transform
).
Here:
function inlineClass(e, {className = "", ...p}, ..c) {
if (!_(e).isString())
return [e, Object.assign(p, {className} ), ...c];
let [element, ...inline] = e.split(".");
className = [className, inline.join(" ")].join(" ");
return [element, Object.assign(p, {className}, ...c];
}
Now we can use it, like this:
let H = hiccupFactory(React, inlineClass);
That’s it!
Flattening Nested Components
Sometimes we just need to wrap components with primitive wrapper components,just so we can do some CSS tricks etc..
Consider this:
[":div",
[":p.wrapper", [":b", "Nested Element"]]]
We can flatten this:
[":div>p.wrapper>b", "Nested Element"]
I first used saw this pattern used in Reagent, and found it very useful.
It’s also very easy to implement:
function flatten(e, p, ...c) {
if (!_(e).isString() || !_(e).contains(">"))
return [e, p, ...c];
let [wrapper, ...nested] = e.split(">");
nested = ":" + nested.join(">");
return [wrapper, flatten(nested, p, ...c)];
}
Auto-generate Keys
In part-2 we’ve already seen how we can overt an ugly JSX example to a simple cond
block for free:
[Content cond(
(this.state.loading), [LoadingMessage],
(items.length === 0), [ItemsList, ZeroItems],
(items.length) , [ItemsList, items.map(i => [Item, {key : i.id}, i]))]
Then with a simple transform function, we got this:
[Content, {placeholder : [this.state.loading, LoadingMessage]},
[ItemsList, {empty : ZeroItems}, items.map(i => [Item, {key : i.id }, i])]]
We can even do something crazier - try to auto-generate keys! because most of the time in my applications keys are derived from .id
s of models, that are either passed as a single child or as a prop named value
.
We can do this:
function keysFromIDs(element,props, ...children) {
if (props.key)
return [element, props, ...children];
if (_(props.value).has("id"))
props.key = value.id;
if (children.length === 1 && _(children[0]).has("id"))
props.key = children[0].id;
return [element, props, ...children];
}
With this, it looks like I can get rid of all the uses of the key
prop In my previous project. I admit this might be too crazy, and might be useless for your own project (in that case you can adjust this).
Still, I persoanly find this kind of code pretty easy to debug when stuff goes wrong (it’s just a function), and easy for other developers to understand and modify (again, it’s just a function). In contrast to the horribly broken “React context” or most “higher order components” or even all the crazy @inject
hacks that many people tend to use.
Also notice that you can implement your own version
of map
that will do this auto key thing, as well as wrap each item in items
with an Item
component.
We can have this:
[Content, {placeholder : [this.state.loading, LoadingMessage]},
[ItemsList, {empty : ZeroItems}, ["#->", Item, items]]
I named it #->
because using ->
or ->>
can cause confusion with the threading macros in clojure,
and because it looks cooler than wrapMap(Item, items)
.
This is how it’s implemented:
let wrapMap => (e, c) c.map(i => [e, _(i).has("id") ? {key : i.id} : null , i]);
function wrapMapTransform(e, p, ...c) {
if (e === "#->")
return wrapMap(...c);
return [e,p,...c];
}
Putting It All Together
I think it’s time to look at one final example, and compare it with JSX, to do that, I just pulled a pretty large UI component from CoolRemote, renamed everything, and inlined all the helper functions.
The result is a weird blog app, where you have a list of articles, a list of users (that only admins care about), current user, and a list of actions.
Here’s an ASCII UI diagram:
["user-1" "user-2" "user-3"] // <-- manage users panel
[+ content] // <-- N data items
[- article]
[- article]
[- article]
["logout" "delete-all", "drat-all"] // <-- panel with actions
The interesting part is that each entity has a loading
state, because we don’t want
to wait until the whole thing loads… and we have permissions, for example
only admin users are allowed to view the “user management panel”.
(The original code had 6 sub-components, and 8 helper functions.)
This is the JSX result:
<div className="app">
{
if (!me.isAdmin)
<UserMenu>
{
(users.loading)
? <LoadingUserMessage />
: users.map(u => <UserMenu.MenuItem key={u.id}>{u}</UserMenu.MenuItem>)
}
</UserMenu>
}
<main className="app-content">
<section className={classnames("group-content", {active : state.active})}>
{
if (state.loading)
<LoadingAppMessage />
else if (items.filter(i => !i.draft).length === 0)
<ZeroItems />
else
items.map(i =>
<article className="item">
{
if (item.loading) {
<LoadingArticleMessage />
} else {
<h2 className="item-title">{i.title}</h2>
(i.subtitle.length > 0) && <h3 className="item-subtitle">{i.subtitle}</h3>
<div className="comments">
<div className="comment-count">{i.comments.length}</div>
</div>
<p className="item-content">{i.content}</p>
}
}
</article>
}
</section>
</main>
<footer>
<ul>
{
actions
.filterByUser(me)
.map(a => <ActionItem key={a.id}>{a}</ActionItem>)
}
</ul>
</footer>
</div>
Can be written like this:
[":div.app"
[UserMenu,
{placeholder : [!users.loading, LoadingUserMessage],
hidden : !me.isAdmin},
["#->" UserMenu.MenuItem, users]],
[":main.app-content",
[":section.group-content",{
className : {active : state.active}
placeholder : [state.loading, LoadingAppMessage],
empty : ZeroItems },
items.map(i =>
[":article.item",
{hidden : i.draft,
key : i.id,
placeholder : [i.loading, LoadingArticleMessage]},
[":h2.item-title", i.title],
[":h3.item-subtitle", {hidden : i.subtitle.length}, i.subtitle],
[":div.comments>div.comment-count", i.comments.length]
[":p.item-content", i.content]])]],
[":footer>ul", ["#->", ActionItem, actions.filterByUser(me)]]]
(If you can write a cleaner version of the JSX above, without helper functions or sub-components, let me know, and I’ll be happy to update this example)
Final Words
Hopefully this blog series made you seriously reconsider JSX, as a “best practice”, and maybe even convinced you of the value of pure data. If you’re impressed with this approach I created a library called react-hut, that implements Hiccup almost like we did, it’s a little bit more opinionated (and faster!), but mostly it’s the same, see if you like it, and if you don’t, it’s very simple to implement your own.
There is also another, much older, project called react.hiccup that implements hiccup for React projects. Unfortunately, it’s not for me, because it has compile-time (with sweet.js, not babel), and It’s harder to extend than a simple function (but it looks more like Clojure).
Finally I strongly encourage you to try Clojure, I think it’s the most pragmatic language for building information systems out there, I see many developers that are intimidated by not having the ugly and familiar c-style syntax, but if you’re reading this, you’re probably open minded enough to not let old habits limit your productivity.
(load :comments)load