A simple, easy to use library for React development in ClojureScript.
(ns my-app.core
(:require [hx.react :as hx]
["react-dom" :as react-dom]))
(hx/defnc MyComponent [{:keys [name]}]
[:div "Hello "
[:span {:style {:font-weight "bold"}} name] "!"])
(react-dom/render
(hx/f [MyComponent {:name "React in CLJS"}])
(. js/document getElementById "app"))
You'll want to make sure you have the latest version of react
, react-is
, and
whatever renderer you are targeting (e.g. react-dom
).
npm i react react-is react-dom
If you want to use the React Hooks API (hx.react.hooks
), you'll need to install
the 16.7.0-alpha version:
npm i react@next react-dom@next react-is
The hiccup interpreter is done other than bugs AFAICT. Please open any issues you find with using components in hiccup forms.
def<thing>
helper macrosThe helper macros are pretty much done, but are subject to change as they are used in anger and either limitations or potential improvements are found.
React Hooks is a new proposal for a way of representing state and other side effects as function calls and data, removing the need for functional components.
These will work out of the box with hx
, but there are some improvements to be
made (IMO) to make them even more ergonomic for developers.
Support for interpretation of hiccup and definition of components in JVM Clojure.
hx
solve?hx
is meant to make it simple, easy and fun to use React.js
within ClojureScript. It is your bridge to the wide world of React.js in
idiomatic CLJS.
The library is split into (currently) two sections, which you can feel free to mix as your project sees fit:
A hiccup interpreter. Takes in [:div {:style {:color "red"}} [:span "foo"]]
and
spits out React.createElement
calls.
Helpers for creating components. defnc
and defcomponent
help us write
plain React.js components in idiomatic ClojureScript.
TL;DR: hiccup is the JSX of the Clojure ecosystem
hx.hiccup
is an implementation of a "hiccup" syntax interpreter.
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 parse
function that takes in a
hiccup form and transforms it into calls to React's createElement
function:
(require '[hx.hiccup :as hiccup])
(hiccup/parse
[ReactComponent {:some-prop #js {:foo "bar"}}
[:div {:class "greeting"} "Hello, ReactJS!"]])
;; executes:
;; (react/createElement ReactComponent #js {:some-prop #js {:foo "bar"}}
;; (react/createElement #js {:className "greeting"}
;; "Hello, ReactJS!"))
The hiccup parser can be extended with custom tags by defining a new
hx.hiccup/parse-element
method. For example, here's how we handle :<>
for
fragments:
(defmethod hiccup/parse-element :<>
[el & args]
(hiccup/-parse-element
hx.react/fragment
args))
hx
not solve?No opinionated state management, no custom rendering queue. Use it to build
your awesome opinionated async reactive immutable app framework. hx
is just
a Clojure-y interface to creating plain, unadulterated React components.
hx
doesn't do anything special in regards to how it calls or creates React
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 MyComponent [props]
(hiccup/parse [:div "Hello"]))
(react-dom/render (hiccup/parse [MyComponent]) (. js/document getElementById "app"))
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 MyComponent [props]
(let [name (goog.object/get props "name")]
(hiccup/parse [:div "Hello, " name "!"]))
(react-dom/render (hiccup/parse [MyComponent {:name "Uma"}])
(. js/document getElementById "app"))
hx.react/defnc
is a macro that shallowly converts the props object for us and
wraps our function body in hiccup/parse
, so we can get rid of some of the
boilerplate:
(hx/defnc MyComponent [{:keys [name]}]
[: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 HasChildren [props]
(let [children (goog.object/get props "children")]
(hiccup/parse
[:div
{:style {:border "1px solid #000"}}
children]))
;; or
(hx/defnc HasChildren [{:keys [children]}]
[:div
{:style {:border "1px solid #000"}}
children])
Sometimes we also need access to React's various lifecycle methods like
componentDidMount
, componentDidUpdate
, etc. In that case, we should create a
React component class. 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 built in.
hx.hiccup
makes several default decisions about how hiccup and components should be
written.
For a number of examples, check out the workshop in the examples folder.
First, all hiccup forms are assumed to follow the same pattern:
[ <(1)elementType> <(2)props-map?> <(3)child> ... <(n)child> ]
.
The element in the (1) first position are assumed to be valid React element types. 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.
One of the built-in React symbols, such as React.Fragment
, a context provider,
context consumer, etc.
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. hx.react/defnc
is used for concision, the only thing
it does is coerce props to a map and wrap the body in a call to hiccup/parse
:
;; has children, but no props. Strings are considered valid children.
(hx/defnc Greet [_]
[:div "Hello"])
;; component that is passed in a map of props
(hx/defnc MediumGreet [_]
[:div {:style {:font-size "28px"}} "Medium hello"])
;; binding props and children to symbols and passing it into the element.
(hx/defnc BigGreet [_]
(let [props {:style {:font-size "56px"}}
children "Big hello"]
[:div props children]))
;; passing in multiple children, and calling components that we defined ourselves
;; (instead of native elements like `:div`).
(hx/defnc AllGreets []
[:div
[Greet]
[MediumGreet]
[BigGreet]])
;; sequences are wrapped in react Fragments for ease of use
(hx/defnc Sequence []
[:div
(for [color ["red" "green" "blue"]]
;; add a key prop to help React
[:strong {:style {:color color} :key color} color])])
;; using children as a function
(hx/defnc FnAsChild [{:keys [children]}]
[:div (children "foo")])
;; passing in children as a function
(hx/defnc UseFnAsChild [_]
[FnAsChild (fn [value]
[:h1 value])])
hx.hiccup
shallowly converts props to a JS object at runtime. Your kebab-case
props will be converted to camelCase before passed into a native element.
:style
is special-cased to recursively convert to a JS obj to help with using
native elements as well.
If all you want is the hiccup interpreter, you may simple import hx.hiccup
:
(require '[hx.hiccup :as hiccup])
(hiccup/parse [:div "foo"]) ;; => {$$typeof: Symbol(react.element), type: "div", key: null, ref: null, props: {…}, …}
hx.react
contains a number of helpful functions and macros for doing React
development in ClojureScript.
An alias of hx.hiccup/parse
, so that if you need to do some ad-hoc
transformation of hiccup one doesn't have to import hx.hiccup
as well as
hx.react
.
This macro is just like defn
, but shallowly converts the props object passed
in to the component to a Clojure map and intelligently interprets the return
value of the body as hiccup. If the body doesn't return a vector, it simply
returns that value.
Takes a name, props bindings and a function body.
Example usage:
(require '[hx.react :as hx])
(require '[hx.hiccup])
(hx/defnc Greeting [{:keys [name] :as props}]
[:span {:style {:font-size "24px"}}
"Hello, " name "!"])
(react/render
(hx.hiccup/parse [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 MyComponent
(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)]
[:div
[:div (. my-component -greeting) ", " (. state -name)]
[:input {:value (. state -name)
:on-change (. this -update-name!)}]])))
An alias for react/createElement
. It will marshall props from a map to JS and
interpret any hiccup children.
Creates a factory function from a component (e.g. a function, class, or string) that, when called, returns a React element.
Short-circuits the hiccup interpreter to return just the hiccup form returned by
body
. Very useful for testing React components created using hx.hiccup
.
Example:
(hx/defnc Welcome [{:keys [age]}]
(if (> age 17))
[:div "You're allowed!"]
[:div [:a {:href "http://disney.com"}] "Please go elsewhere"])
;; in test
(deftest welcome-allowed
(is (= (hx/shallow-render (Welcome {:keys age}))
[:div "You're allowed!"]))
(is (= (hx/shallow-render (Welcome {:age 17}))
[:div [:a {:href "http://disney.com"}] "Please go elsewhere"])))
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