An easy to use, decomplected hiccup compiler for ClojureScript and React.
(require '[hx.react :as hx])
(require '[react :as react])
(react/render
(hx/c
[:span {:style {:font-weight "bold"}} "Hello, world!"])
(. js/document getElementById "app"))
hx
solve?TL;DR: hiccup is the JSX
of the Clojure ecosystem, and hx
aims to solve that problem just as well.
hx
is an implementation of a "hiccup" syntax compiler. Hiccup is a way of
representing HTML using clojure data structures.
It uses vectors to represent elements, and maps to represent an elements
attributes.
The original hiccup library was written for Clojure and outputs HTML strings. This library is written for use in CLJS and outputs React data structures. It extends the syntax slightly to accomodate using any arbitrary React component in place of HTML tags.
The basis of the library is the compile-hiccup
function that takes in a
hiccup form and transforms it into calls to React's createElement
function:
(require '[hx.compiler.core :refer [compile-hiccup]])
(compile-hiccup
[ReactComponent {:some-prop #js {:foo "bar"}}
[:div {:class "greeting"} "Hello, ReactJS!"]]
'react/createElement)
;; => (react/createElement ReactComponent #js {:some-prop #js {:foo "bar"}}
;; (react/createElement #js {:className "greeting"}
;; "Hello, ReactJS!"))
This is then used to build a macro so that it can be used on our CLJS code
at compile time. hx
comes with it's own macro out of the box with sane
defaults, but the core compiler is also available should you have different
needs.
hx
not solve?No state management, no custom rendering queue, no opinions. Use it to build
your awesome opinionated async reactive immutable app framework. hx
is just
your plain, unadulterated hiccup → React library.
Simple interop between vanilla React features and CLJS code, to ease adoption of new features & technologies in the JS world.
Performant parsing of hiccup syntax; impact is minimized by using macros, to remove the need for runtime parsing of hiccup and minimize marshalling of CLJS data.
Extensible API so that parsing, analysis & code generation of the hiccup compiler can evolve to meet the needs of different ecosystems.
There are a lot of cool things coming out of React 16 that are contesting some initial design decisions of other React wrappers in the CLJS ecosystem:
Up until now, CLJS wrappers have been implementing async rendering in user-land. Firstly, async rendering is generally a good thing.
Now that React is implementing async rendering in the framework itself, we should endeavor to leverage the framework rather than our various user-land implementations.
Secondly, the second-half of the Dan Abramov video above is quite impressive, and I am very excited about React "suspense." Again, this is something that we have solved many times in CLJS-land, but once this lands there will be an explosion of features & functionality that we will not be able to access easily unless we can bind closely to this new API.
Thirdly, React's new context API greatly simplifies passing state around an app in an async-safe way. This is especially important when considering server-side rendering, a use-case that many CLJS libraries still do not weigh very heavily. Removing the layers of abstraction between CLJS & vanilla React is important for using React context and (more generally) render-props/function-as-children.
Some frameworks such as Reagent, Rum, etc. define their own way of parsing hiccup and creating React elements. While this allows them to build integrations, it also means that our code must subscribe to many ways in which these frameworks control our application code. We can combine them at the seams, but doing a full-on replacement is often difficult.
hx
aims to not control state management, rendering, or anything else about
your application. It should only give you a way of describing React data easily
in your CLJS applications.
Sablono and Hicada
are two other great libraries for parsing & compiling hiccup syntax into React
components. hx
is different in two significant ways:
A uniform syntax for calling React components (as in, functions and React obj).
No need to constantly mix [:div ..]
with (my-component ...)
, creating
factories, etc.
No runtime interpretation of hiccup syntax; always assumes that things are tags or React elements.
Out-of-the-box defaults allow the library to be easily used right away, while providing APIs to extend and change the parsing, analysis and generation of hiccup → React elements as your needs evolve.
hx
makes several default decisions about how hiccup and components should be
written.
First, all hiccup forms are assumed to follow the same pattern:
[ <(1)component> <(2)props-map?> <(3)child> ... <(n)child> ]
.
The element in the (1) first position are assumed to be valid React elements. They can be:
A keyword that maps to a native element - such as :div
, :span
, :nav
.
These are mapped to a string and passed into createElement
.
A function that returns React elements, aka a functional component.
An object that extends the React.Component class.
Regardless of the type of the element in position (1), if you want to pass in props to a component, they are always passed in via a map in the (2) second position. Props can be written as a map literally or a symbol bound to a map. If the element in position (2) is not a map, it is assumed to be a child and passed into the children field of createElement.
Finally, anything passed into position (3) or on is considered a child element.
Here are a few examples:
;; (1)
(defn greet []
(hx/c [:div "Hello"]))
;; (2)
(defn medium-greet []
(hx/c [:div {:style {:font-size "28px"}} "Medium hello"]))
;; (3)
(defn big-greet []
(let [props {:style {:font-size "56px"}}
children "Big hello"]
(hx/c [:div props children])))
;; (4)
(defn all-greets []
(hx/c
[:div
[greet]
[medium-greet]
[big-greet]]))
(1) is an example of writing a component that has children, but no props. Strings are considered valid children.
(2) is an example of writing a component that is passed in a map of props.
(3) is an example of binding props and children to symbols and passing it into the element.
(4) is an example of passing in multiple children, and calling components that we
defined ourselves (instead of native elements like :div
).
Let's pause here and talk about the difference between (2) and (3) in the example above. Functionally, they are equivalent, but there are some things that the compiler will do differently depending on whether a symbol or map literal is passed in as the second element.
React's createElement
function expects props to be passed in as a JS object.
It will attempt to introspect this object. So we need to marshall the props map into
a JS object before we can pass it off to React.
The hx
compiler attempts to be clever: when it detects that the second argument is
a map literal, it will shallowly rewrite it into a native JS object:
(hx/c [:div {:foo "bar"
:baz {:asdf ["jkl" 1234]}}])
;; =>
(React/createElement
"div"
(js-obj "foo" "bar"
"baz" {:asdf ["jkl" 1234]}
nil)
It only rewrites the first level; any nested structures are left untouched.
--
Sidenote: If you're working with a vanilla React component (implemented in JS), you may have to write something like this to convert the nested structures into native JS types:
(hx/c [SomeWidget {:config #js {:foo "bar" :baz #js ["jkl" 1234]}}])
Currently, :style
is special cased where it will recursively marshall it so that it's
easy to work with native elements. You won't have to do this with :style
, but any
other props will need this manual conversion.
--
If the compiler doesn't see a map literal in the second position, it effectively treats it as a child element and simply passes it through unchanged.
As a convenience, though, if props are nil
, hx.react
will check if the first child
is a map, and if so, shallowly convert it to a JS object at runtime. There should be no
functional difference between doing this at runtime vs. compile-time, but there may be
a slight performance hit. In most cases, this will be unnoticeable; however if you have
a component that is on the hot path and the marshalling does become a performance
bottleneck, writing out props as a map literal will improve it.
hx
doesn't do anything special in regards to how it calls or creates CLJS components.
They are assumed to act like native, vanilla React components that could be used in any
codebase.
In practice, this is fairly easy to handle in ClojureScript. A basic functional component can be written as just a normal function that returns a React element:
(defn my-component [props]
(hx/c [:div "Hello"]))
props
will always be a JS object, so if we want to pull something out of it, we'll
need to use JS interop:
(defn my-component [props]
(let [name (goog.object/get props "name")]
(hx/c [:div "Hello, " name "!"]))
hx.react/defnc
is a macro that shallowly converts the props object for us, so
we can get rid of some of the boilerplate:
(hx/defnc my-component [props]
(let [name (:name props)]
(hx/c [:div "Hello, " name "!"])))
Children are also passed in just like any other prop, so if we want to obtain children we simply peel it off of the props object:
(defn has-children [props]
(let [children (goog.object/get props "children")]
(hx/c [:div
{:style {:border "1px solid #000"}}
children]))
;; or
(hx/defnc has-children [props]
(let [children (:children props)]
(hx/c [:div
{:style {:border "1px solid #000"}}
children])))
Sometimes we also need access to React's various lifecycle methods like
componentDidMount
, componentDidUpdate
, or we need to re-render our component
based on some internal state. In that case, we should create a React component
class. This is mainly left as an exercise to the reader; hx
exposes a very
barebones hx/defcomponent
macro that binds closely to the OOP, class-based
API React has for maximum flexibility. You can also leverage libraries like
Om.Next, Reagent, Rum, or other frameworks that have state management buil in.
PRs welcome for libraries built on top of hx
for managing this in a more idiomatic
Clojure way!
This top-level macro is meant to serve as sane defaults for users (app developers, library developers) to use out-of-the-box. It provides a good mix of performance, ease of use and interoperability.
This macro takes in form as hiccup and transforms it into React.createElement calls.
Example usage:
(require '[hx.react :as hx])
(let [numbers [1 2 3 4 5]]
(hx/c [:ul {:style {:list-style-type "square"}}
(map #(hx/c [:li {:key %} %])
numbers)]))
Will become the equivalent:
(let [numbers [1 2 3 4 5]]
(react/createElement "ul" #js {:style #js {:listStyleType "square"}}
(map #(react/createElement "li" #js {:key %} %)
numbers)]))
Along with compilation of hiccup into React API calls, it also comes with a few other helpful macros & functions for creating React components. It handles shallowly marshalling props into CLJS data structures and some other quality of life improvements.
Feel free to ignore them if you want to build something cooler.
This macro is just like defn
, but has some helpers for defining functional
React components. Takes a name, props bindings and a body that will be passed to
hx.react/compile
.
Example usage:
(require '[hx.react :as hx])
(hx/defnc greeting [{:keys [name] :as props}]
(hx/c [:span {:style {:font-size "24px"}}
"Hello, " name "!"]))
(react/render
(hx/c [greeting {:name "Tara"}])
(. js/document getElementById "app"))
This macro creates a React component class. Is the CLJS equivalent of
class {name} extends React.Component { ...
. constructor
is passed in this
and must return it. Additional methods and static properties can be passed in,
similar to defrecord
/ deftype
. Methods are automatically bound to this
.
Example usage:
(hx/defcomponent my-component
(constructor [this]
(set! (. this -state) #js {:name "Maria"})
this)
^:static
(greeting "Hello")
(update-name! [this e]
(. this setState #js {:name (.. e -target -value)}))
(render [this]
(let [state (. this -state)]
(hx/c [:div
[:div (. my-component -greeting) ", " (. state -name)]
[:input {:value (. state -name)
:on-change (. this -update-name!)}]]))))
Compiles a hiccup
form into function calls to the passed in create-element
symbol.
Example usage:
(require '[hx.compiler.core :refer [compile-hiccup]])
(compile-hiccup
[ReactComponent {:some-prop #js {:foo "bar"}}
[:div {:class "greeting"} "Hello, ReactJS!"]]
'react/createElement)
;; => (react/createElement ReactComponent #js {:some-prop #js {:foo "bar"}}
;; (react/createElement #js {:className "greeting"}
;; "Hello, ReactJS!"))
STUB
Copyright © 2018 Will Acton
Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close