Reagent & Hiccup
Let's start this post by looking at React's hello world example:
ReactDOM.render(
<h1 className="welcome">Hello, world!</h1>, // <-- JSX
document.getElementById('root')
);
and now let's rewrite it in Reagent (a popular ClojureScript React wrapper)
(reagent.dom/render
[:h1 {:class "welcome"} "Hello, world!"] ; <-- Hiccup
(.. js/document (getElementById "root")))
If the above is the first time you're reading ClojureScript or Reagent, it may look foreign, but you might also notice that the overall shape of the code (lines, structure, functions) is more or less the same. To me, the biggest difference is what happens on line 2
[:h1 {:class "welcome"} "Hello, world!"]
The above, my friends, is Reagent Hiccup
and it's a common way to represent HTML in Clojure. In this way, it's Reagent's version of JSX. Just to provide more context, here is another example of Reagent Hiccup
:
[:ul {:class "list"}
[:li {:class "list-item"} "Item 1"]
[:li {:class "list-item"} "Item 2"]
[:li {:class "list-item"} "Item 3"]]
If you're like me when I first started writing ClojureScript, Hiccup
can seem a little confusing. How is it possible to use Hiccup
without importing a library or adding a plugin to our build tools? How does React know what to do with Reagent Hiccup
? These, and more, are questions I hope to answer in this post.
Reagent Hiccup
Let's return to the code snippet we started this post with:
(reagent.dom/render
[:h1 {:class "welcome"} "Hello, world!"] ; <-- hiccup
(.. js/document (getElementById "root")))
On line 2 we have an unassuming Clojure vector, or is it? In truth, that's exactly what it is. It's just a vector, but it's also known as Reagent Hiccup
. This might lead one to ask, "If it's just a vector, how is it also Reagent Hiccup
?".
The reason is because reagent.dom/render
, the entry point for a Reagent app, accepts either Reagent Hiccup
or a React Element
as the first argument. So by providing a vector
, Reagent automatically treats it like Reagent Hiccup
. This means that Reagent also expects it to be written in a specific way.
To be considered valid Reagent Hiccup
, the vector you pass to Reagent needs to take one of the following shapes:
[tag]
; => [:h1]
[tag attributes]
; => [:h1 {:class "welcome"}]
[tag children]
; => [:h1 "Hello world!"]
[tag attributes children]
; => [:h1 {:class "welcome"} "Hello world!"]
Here is another way to break it down:
- tag -
:h1
- attributes -
{:class "welcome"}
- children -
"Hello world!"
Thus, if we were to pass something that's not actually Reagent Hiccup
, Reagent is kind enough to throw a JavaScript assertion error in the browser console letting us know what went wrong. For example, an empty vector would result in a console assertion error being thrown.
What allows Reagent to understand Hiccup without us needing to import a library or add a plugin to our build tools? It's because Reagent comes with a Hiccup compiler built-in. This will be covered in more detail in the next section.
Reagent Hiccup to React Element
As we mentioned, all components are passed into reagent.dom/render
and it's this function that's responsible for turning Hiccup into something that React understands.
The process begins by Reagent passing the component
given to reagent.dom/render
to a function called create-class
.
create-class
has other jobs aside from handling Hiccup, but nonetheless one of it's jobs is to compile Reagent Hiccup
to React.createElement
calls. This step is handled by the as-element function.
as-element
accepts Reagent Hiccup
like this:
[:h1 {:class "welcome"} "Hello, world!"]
Compiles it to React.createElement
function calls like this:
React.createElement(
"h1",
{className: 'welcome'},
"Hello, world!"
);
The above is given to React which actually runs the React.createElement
calls turning them into React Elements
like this:
{
type: "h1",
props: {
className: 'greeting',
children: "Hello, world!"
}
};
which ultimatley gets turned into HTML
<h1 class="welcome">Hello, world!</h1>
Understanding that everything is turned into React.createElement
calls you might be asking, "Could we just use React.createElement and not use Reagent Hiccup?". The answer? Yup!
Reagent Without Hiccup
Similar to React and JSX, you can use plain old React.createElement
to create Reagent Components
. For example, where we wrote the original example as:
(reagent.dom/render
[:h1 {:class "welcome"} "Hello, world!"]
(.. js/document (getElementById "root")))
we could also use reagent.core/create-element
to write a Reagent component
like this:
(reagent.dom/render
(reagent.core/create-element
"h1"
#js{:className "welcome"}
"Hello, world!")
(.. js/document (getElementById "root")))
Thus, the following are equivalent
; hiccup
[:h1 {:class "welcome"} "Hello, world!"]
; reagent function
(reagent.core/create-element
"h1"
#js{:className "welcome"}
"Hello, world!")
Understanding this, are there any reasons to use reagent.core/create-element
over Reagent Hiccup
?
From a technical perspective, there aren't any noticeable benefits. The advantages would be felt at an individual developer level based on their preferences.
For example, one might suggest that using reagent.core/create-element
over hiccup
initially feels easier to teach new developers how to create components in React/Reagent.
At this point, I feel it's a good time to go into why we use Reagent Hiccup
at all.
Why Reagent Hiccup?
As we can see, Reagent Hiccup
is the Clojure(Script) equivalent of JSX. While they look different, they are both Domain Specific Languages (DSLs) which allow us to represent HTML in Clojure and JavaScript respectively.
The reason we use Hiccup in Reagent is similar to the reasons for using JSX in React.
- Separation of concerns: Separate by component vs. technology
- Accessibility: easier to read and write than
React.createElement
- Expressivity: it's a Clojure data structure, so we have the full power of Clojure
The second point, Accessibility, is particularly interesting. One of the things I mean by this is that because Hiccup is a popular DSL in Clojure land, and not specific to Reagent
, it can be quickly be adopted by developers already familiar with Hiccup.
Hiccup and Clojure
You may have noticed that I've been writing Reagent Hiccup
instead of just Hiccup. The reason for this is because, as mentioned above, Hiccup is not specific to Reagent
.
Hiccup was introduced by James Reeves and has become a standard DSL for Clojure developers looking to represent HTML in Clojure. This means that there are many 3rd party libraries which allow you to write Hiccup in your app even if you aren't using Reagent. For example, the following are all examples of popular Hiccup libraries.
And I think this is the beginning of some of the confusion when it comes to Hiccup and Reagent.
When one begins to learn ClojureScript, they often start with Clojure. The reading material for Clojure, in the form of guides, references and libraries, is larger and oriented toward Clojure. This makes sense for a number of reasons, but it also creates a challenge when trying to figure out how to use HTML in Clojure.
So, you start going through community resources and see that the answer is Hiccup. You pick up one of the above libraries, create a demo app and things are great. Now that you made something happen, you start to experiment with Reagent and notice that you are some how just able to write Hiccup without importing a library and this can seem like "magic".
Eventually you figure it out, but it can be a bumpy road. This is why I decided to write a little about this because I always find it easier to conceptualize what I am doing when I understand how the pieces fit together.
Conclusion
The overarching point is that Hiccup is a common way of writing HTML is Clojure. Unlike JSX, which for a long while was a React only thing, Hiccup is not specific to Reagent/React and as a result, it can be confusing to understand where it all connects.
That's all for this post, but if you are interested in learning more about Hiccup, please take a read through his article on hiccup. For everyone else, thanks for reading along and I hope this has helped demystify some of the inner workings of Reagent without gettting us too lost in the weeds.