Awesome
cljsx
The Missing JSX Clojure Macro
cljsx
tries to make it as easy as possible to use plain, unwrapped React
(or any other virtual dom) and all the related JavaScript libraries in
ClojureScript by mimicking the syntax of JSX. It's mainly meant to be used
with the amazing shadow-cljs with its effortless usage of plain NPM packages,
but it works just as well with Figwheel and Clojure.
TL;DR
It's just a macro which transforms occurences of JSX expressions like
(<div :className "foo" > "Hello" "World!")
into
(createElement "div" {:className "foo"} "Hello" "World!")
.
cljsx
doesn't really care about what createElement
points to. It only tries
to figure out whether it's a JavaScript or Clojure function so it can convert
the arguments with clj->js if needed.
Features
- Not yet another React wrapper, just a macro.
- Works with all vdom libraries which have the
createElement
signature. - JSX expressions can appear anywhere and can be arbitrarily nested. That's why it works so well with libraries like React Router or Material-UI. Whole namespaces can be wrapped in the macro.
- Has props spread operator similar to
<div {...props} />
. - Supports truthy prop shorthand as in
<button disabled />
. - Automatically converts from and to JS if needed.
- Built with spec, so you'll know early when something goes wrong.
Let's say you want to write a SPA directly in React, not using any wrapper.
Maybe you want to use React Router or some other JavaScript library
and you don't want to be dealing with incompatibility issues with wrappers like reagent.
You would directly use the react/createElement
function which you assign to h
for brevity:
(ns shadow-cljs-example.main
(:require ["react" :as react]
["react-dom" :as react-dom]
["react-router-dom" :as rr]))
(def h react/createElement)
(react-dom/render
(h rr/BrowserRouter nil
(h rr/Route nil
(fn [route-props]
(h react/Fragment nil
(h "h1" nil (-> route-props .-location .-pathname (subs 1)))
(h "ul" nil
(map #(h "li" #js{:key %}
(h rr/Link #js{:to %} %))
["foo" "bar" "baz"]))))))
(js/document.querySelector "#mount-point"))
The cljsx.core/jsx>
macro call in the following example just expands
to something very similar and equivalent to the example above,
except that the code is a bit more readable and looks more familiar
to someone with React background.
(ns shadow-cljs-example.main
(:require ["react" :refer [createElement Fragment]]
["react-dom" :as react-dom]
["react-router-dom" :as rr]
[cljsx.core :refer [jsx> fn-clj]]))
(jsx>
(react-dom/render
(<rr/BrowserRouter>
(<rr/Route>
(fn-clj [{{path :pathname} :location}]
(<>
(<h1> (subs path 1))
(<ul>
(map #(<li :key % >
(<rr/Link :to % > %))
["foo" "bar" "baz"]))))))
(js/document.querySelector "#mount-point")))
And this is what it would look like written in reagent.
Notice the :>
in front of every React Router component,
the need to wrap the return value of the rr/Route
callback
in r/as-element
and also, that using the anonymous function
shorthand is a bit awkward with the hiccups vector markup,
all of which just add noise to the code.
(ns shadow-cljs-example.main
(:require [reagent.core :as r]
["react-router-dom" :as rr]))
(r/render
[:> rr/BrowserRouter
[:> rr/Route
(fn [route-props]
(r/as-element
[:<>
[:h1 (-> route-props .-location .-pathname (subs 1))]
[:ul (map #(identity ^{:key %} [:li
[:> rr/Link {:to %} %]])
["foo" "bar" "baz"])]]))]]
(js/document.querySelector "#mount-point"))
Motivation
If you think about it, JSX is just a reader macro, which merely adds syntactic sugar to JavaScript. Oddly enough in ClojureScript, the problem of working with React is mostly approached by inventing all sorts of wrappers, which more often than not come bundled with their own state managemet. The most idiomatic way to express React DOM trees in Clojure seems to be the hiccups format of nested vectors, which probably stems from the obsession with data in Clojure (list's don't seem to be data enough).
cljsx.core/jsx>
is the missing macro. It's the main workhorse of the package
alongside a bunch of helpers for taming the JavaScript conversion.
The main goal of the package is expressiveness, readability and familiarity with
React idioms. cljsx
is not tied to React in any way other than through the
signature of the createElement
function and it works with all JSX
compatible libraries like Inferno, Nerv, Preact or Snabbdom.
Usage
The main thing cljsx
provides is the jsx>
macro and its brethren react>
,
snabbdom>
, inferno>
etc. The macro will recognize JSX expressions in the
code passed to it and will expand them into createElement
calls. Anything else
will be kept unchanged:
(macroexpand (cljs/jsx> (<div>)))
;; => (createElement "div" nil)
So if you want the above example to work with React, you should refer
createElement
from the react
namespace. Similarly, if you intend to use the
<>
fragment tag, you should also refer the Fragment
name from the same
namespace.
(ns my-react-app.main
(:require [cljsx.core :refer [jsx>]]
[react :refer [createElement Fragment]]
[react-dom :as react-dom]))
(react-dom/render
;; The JSX macro call
(jsx>
;; This s-expression is recognized as a JSX expression, because the item
;; at the function call position is recognized as a JSX tag by the macro.
(<>
;; This is not recognized as a jsx-expression, because the first item of
;; the s-expression is not a JSX tag.
(str "Hello, ")
;; This is a JSX expression, because the first item is a JSX tag.
(<h1> "CLJSX!")))
(js/document.querySelector "#mount-point"))
;; The jsx> macro call expands to
(createElement Fragment nil
(createElement "h1" nil "Hello, CLJSX!"))
The cljsx.core/jsx>
macro expands to createElement
and Fragment
calls, which in
JSX parlance is called pragma. If you will be using cljsx
with React, you
can save some keystrokes by using the cljsx.core/react>
macro, which works exactly
as the jsx>
macro, except that the pragma is bound to react/createElement
and react/Fragment
.
(ns my-react-app.main
(:require [cljsx.core :refer [react>]]
[react :as react]
[react-dom :as react-dom]))
(react-dom/render
(react>
(<>
(<h1> "Hello, CLJSX!")))
(js/document.querySelector "#mount-point"))
There are other macros with pragmas bound to other JSX compatible frameworks.
inferno>
bound toinferno-create-element/createElement
andinferno/Fragment
.nervjs>
bound tonervjs/createElement
andFragment
preact>
bound topreact/createElement
andFragment
snabbdom>
bound tosnabbdom/createElement
andFragment
Notice that the last three macros are bound to unprefixed Fragment
. This is
because none of these frameworks seems to actually have a notion of a fragment.
Having it bound to Fragment
allows you to choose how the fragment will be
interpreted. You could for example fall back to a div with
(def Fragment "div")
or make it a function which throws an exception, or just
let it fail on the undeclared Fragment
var.
You can even create your own JSX macro with the cljsx.core/defjsx
macro, which is
how all the aforementioned macros are defined. Note that as defjsx
is a macro
which creates macros, it needs to be called in a clj
or cljc
file.
;; my_jsx_app/main.clj
(ns my-jsx-app.main
(:require [cljsx.core :as cljsx]))
(cljsx/defjsx my-jsx-macro my-create-element my-fragment)
(def my-fragment :fragment)
(defn my-create-element [tag props & children]
{:tag tag
:props props
:children children})
(my-jsx-macro
(<>
(<h1> "Hello, CLJSX!")))
;; The above statement returns
{:tag :fragment
:props nil
:children [{:tag "h1"
:props nil
:children ["Hello, CLJSX!"]}]}
What is a JSX Expression?
Any list whose first element is a JSX tag is recognized as a JSX expression.
What is a JSX Tag?
Any symbol in the function call position starting with the <
character,
followed by an alphanumeric character and optionally ending with the >
character is a JSX tag. The string of characters between the opening <
and
the optional closing >
is the tag name e.g. <>
, <a>
, <div
, <foo>
,
<bar
, <Baz>
, <Bing
, <foo.bar/Baz>
, <foo/bar.Baz
. The following are
still JSX tags even though they have invalid names: <foo.bar>
, <<>
,
<..foo>
. This is to fail early on spec violation, as opposed to undeclared
var errors.
Intrinsic Tags
JSX tags
with all lowercase, alphanumeric names are intrinsic and will
expand to a createElement
call with the tag name in a string literal e.g.
<div>
, <a>
, <h1>
, <span>
, <foobarbaz>
.
(macroexpand (cljsx/jsx> (<foo>)))
;; => (createElement "foo" nil)
Reference Tags
JSX tags
whose names are capitalized e.g. <Foo>
are reference tags and
will be expanded with the name as symbol. Tag names can be namespaced in the
same way as symbols with the only exception, that the last part must be
capitalized e.g. <foo.bar/Baz>
, <foo/bar.Baz>
, in which case they are also
considered to be reference tags.
(macroexpand (cljsx/jsx> (<Foo>)))
;; => (createElement Foo nil)
(macroexpand (cljsx/jsx> (<foo.bar/Baz>)))
;; => (createElement foo.bar/Baz nil)
(macroexpand (cljsx/jsx> (<foo/bar.Baz>)))
;; => (createElement foo/bar.Baz nil)
Invalid Tags
Anything else is recognized as an invalid tag and will be reported by the spec during compile time.
Simple JSX Expressions Without Props
If a tag ends with the >
character, it has no props or attributes and the
JSX expression expands to a createElement
call with nil
as the second
argument.
(macroexpand (cljsx/jsx> (<foo>)))
;; => (createElement "foo" nil)
(macroexpand (cljsx/jsx> (<Bar>)))
;; => (createElement Bar nil)
JSX Expressions With Props
If the tag does not end with the >
character, the jsx>
macro will
will expect a sequence of props after the tag, terminated by the >
symbol.
A prop is either:
- A single keyword
- A keyword followed by a non-keyword expression
- The
...
symbol followed by a spreadable expression.
Note that cljsx
doesn't do any prop name conversion e.g. hyphenization, etc,
as it is none of its business. If you wan't to write React props hyphenized,
or, you wan't to use :class
instead of :className
, you can wrap
react/createElement
in your own createElement
implementation
where you can intercept the props and forward them transformed to React:
(ns shadow-cljs-example.main
(:require ["react" :as react]
["react-dom" :as react-dom]
[camel-snake-kebab.core :as csk]
[cljsx.core :as cljsx]))
(defn createElement [tag props & children]
(apply react/createElement
tag
(->> props
(reduce-kv #(assoc %1 (csk/->camelCase %2) %3) {})
clj->js)
children))
(cljsx/jsx>
(react-dom/render
(<div :class-name "foo"
:on-click #(println "clicked") >
"Bar")
(js/document.querySelector "#mount-point")))
Key-value Props
A key-value prop is a keyword followed by any non-keyword expression e.g
:foo 123
, :bar baz
, :bing (+ 1 2 3)
. JSX expressions with this kind of
props will be expanded to a createElement
call with the second argument being
a map of the props.
(cljsx/jsx>
(<a :href "http://example.com" :target "_blank" >))
;; Expands to
(createElement "a" {:href "http://example.com" :target "_blank"})
(cljsx/jsx>
(<button :onClick #(println "click")
:disabled (not enabled) >))
;; Expands to
(createElement "a" {:onClick #(println "click")
:disabled (not enabled)})
Shorthand Truthy Props
As JSX, cljsx
supports shorthand truthy props, which is any keyword
followed by either another keyword or the >
or ...
symbols.
A shorthand prop will be expanded to a value of true
.
(cljsx/jsx>
(<button :disabled :onClick #(println "click") >))
(cljsx/jsx>
(<button :onClick #(println "click") :disabled >))
;; Both expand to
(createElement "button" {:disabled true :onClick #(println "click")})
The ...
Spread Operator
As JSX, cljsx
supports spreading a map (or anything spreadable) to the
props. Anything followed by the ...
symbol will be merged with the rest of
the props using merge in the order
they appear in the props sequence.
(def props {:baz 33})
(cljsx/jsx>
(<Foo :foo 11
:bar 22
... {:foo 111}
:baz 33
... props
... [:bing 444]
:bing >))
;; Expands to
(createElement Foo (merge {:foo 11 :bar 22}
{:foo 111}
{:baz 33}
props
[:bing 444]
{:bing true}))
Children
All expression after a simple tag or the first >
symbol after a props tag
in a JSX expression are treated like children and are added as parameters
right after props to the expanded JSX expression.
(cljsx/jsx>
(<div> "Hello, World!"))
;; Expands to
(createElement "div" nil "Hello, World!")
(cljsx/jsx>
(<span :style {:color "aqua"} > "Hello," " Aqua" " World!"))
;; Expands to
(createElement "span" {:style {:color "aqua"}} "Hello," " Aqua" " World!")
Fragment
As JSX, cljsx
supports the <>
fragment tag and as in JSX it can't have
props. A JSX expression with a fragment tag expands to a createElement
call with the Fragment
symbol as the tag argument.
(cljsx/jsx>
(<ul>
(<>
(<li> "Foo")
(<li> "Bar")
(<li> "Baz"))))
;; Expands to
(createElement "ul" nil
(createElement Fragment nil
(createElement "li" nil "Foo")
(createElement "li" nil "Bar")
(createElement "li" nil "Baz")))
If you need to pass props to the tag, e.g. the key
prop in React,
you can do it by using Fragment
directly without the <>
syntax.
(cljsx/jsx>
(<Fragment :key 123 >
(<div> "Foo")))
;; Expands to
(createElement Fragment {:key 123}
(createElement "div" nil "Foo"))
Nesting
JSX expressions can appear anywhere inside of the macro and can be arbitrarily deeply nested. They can appear as values in all the datastructure literals like lists, vectors, maps, sets. They can even appear as keys in maps. Moreover, they can appear as prop values, which is a common pattern in React libraries like React Router and Material-UI. You can even use it as values in spreads as long as it returns a value accepted by merge.
(let [Header (fn [{:keys [children]}]
(<h1> "Hello, " children "!"))
Body (fn [{:keys [items]}]
(<ul>
(map #(<li :key (str (first %)) >
(second %))
items)))
footer (<h3> "Enjoy!")]
(<div>
(<Header> "CLJSX")
(<Body :items {:foo (<h1> "Foo")
:bar (<h2> "Bar")
:baz (<h3> "Baz")} >)
footer))
The jsx>
macro accepts any number of expressions. Multiple expressions
will be expanded wrapped in a do
form. You can actually wrapp a whole namespace in the macro:
(ns my-shadow-cljs-react-app.main
(:require ["react" :refer [createElement Fragment]]
["react-dom" :refer [render]]
[cljsx.core :refer [jsx>]]))
(jsx>
(defn Header []
(<h1> "Hello, CLJSX!"))
(defn Body []
(<p> "Lorem ipsum dolor sit amet."))
(defn Footer []
(<h3> "Enjoy!"))
(defn App []
(<div>
(<Header>)
(<Body>)
(<Footer>)))
(render
(<App>)
(js/document.querySelector "#mount-point")))
Escaping
The jsx>
macro uses some symbols as part of its DSL like the ...
as
the props spread operator, and the >
as the end of props tag.
It also specially treats symbols on the function call position whose names start
with the <
character, which it recognises as JSX tags.
The macro doesn't have any special escape mechanism, but it relies on what's
already available in Clojure. So if you need to pass the >
function as a value
of a prop, or you happen to have a var assigned to a symbol starting with the
<
character, you can dereference the vars with the @
reader macro:
(ns my-shadow-cljs-react-app.main
(:require ["react" :refer [createElement Fragment]]
["react-dom" :refer [render]]
[cljsx.core :refer [jsx>]]
;; You can not use the ... symbol in def, but you can import it.
[made-up-three-dots-exporting-namespace :refer [...]]))
(defn <div> [] "My name is <div>")
(def <a> "My name is <a>")
(defn SortableList [{:keys [comparator items]}]
(jsx>
(<ul>
(map #(<li> %) (sort comparator items)))))
(jsx>
(render
;; Would be interpreted as props spread operator if not escaped.
(<div :title @#'... >
(<SortableList :items ["a" "b" "c"]
;; Would be interpreted as JSX tag end if not escaped.
:comparator @#'> >)
;; Would be interpreted as a JSX tag if not escaped.
(@#'<div>)
;; No need to escape if not in a function call position.
<a>)
(js/document.querySelector "#mount-point")))
Passing Keywords As Prop Values
Because of the truthy prop shorthand you can not pass a keyword literal as a prop value. You can however pass any expression that evaluates to a keyword.
(cljsx/jsx>
(<Foo :key :val >))
;; Expands to this, which is probably not what you wanted
(createElement Foo {:key true :val true})
But you can pass a keyword constructed with the keyword
function, or any other
expressions which returns a keyword. Or even better spread a
keyword-to-keyword map into the props:
(cljsx/jsx>
(<Foo :foo (keyword "foo")
:bar (identity :bar)
:baz (do :baz) >)
(<Foo ... {:foo :foo
:bar :bar
:baz :baz} >))
;; Both expand to
(createElement Foo {:foo :foo
:bar :bar
:baz :baz})
Note that since the component calls are delegated to the createElement
function, having components which expect keywords or other Clojure specific
types as prop values only makes sense if the createElement
function is a
Clojure function. For example react/createElement
is expecting all its
arguments to be JavaScript objects and cljsx
will do this conversion under the
hood. React will then call your component with the converted JavaScript values
and since the conversion is done with clj->js the keywords will become
strings. Read further about the JavaScript conversion in the next section.
JavaScript Conversion
More likely than not, you will be using cljsx
with JavaScript libraries e.g.
React, which expect JavaScript values as their arguments. So if you wanted to
use react/createElement
in ClojureScript directly you would need to pass the
props as a plain JavaScript object:
;; You need to pass the props as JavaScript,
;; either by using the #js reader macro,
(react/createElement "h1" (clj->js #js{:style #js{:color "blue"}})
"Kind of blue")
;; or converted with clj->js
(react/createElement "h1" (clj->js {:style {:color "blue"}})
"Kind of blue")
If and only if the jsx>
macro is used in ClojureScript environment, its
expansion will include JavaScript conversion code:
(cljsx/jsx>
(<div :className "foo"
... parent-props >
"Foo"))
;; In a Clojure environment, the above snippet expands to:
(createElement Component
(merge {:foo "bar"} parent-props)
"Baz")
;; Whereas in ClojureScript, the expansion includes JS conversion code
(if (cljsx.core/js? createElement)
;; If createElement is a JavaScript function,
(apply createElement
;; If the tag (in this case Component) is a JavaScript function,
(if (cljsx.core/js? Component)
;; no conversion is needed, because the props are a JS object already.
Component
;; Otherwise (if it is a Clojure function),
;; the tag function needs to be intercepted,
(fn [props__38180__auto__]
;; so the props passed by the JS createElement function can be
;; converted to a Clojure map so they don't need to be manually
;; converted in each component implementation.
(Component (js->clj props__38180__auto__
:keywordize-keys true))))
;; The props and children both need to be converted to JS before they
;; are passed to the JS createElement
(clj->js (merge {:foo "bar"} parent-props))
(map clj->js ["Baz"]))
;; If createElement is a Clojure function, no conversion happens.
(createElement Component
(merge {:foo "bar"} parent-props)
"Baz"))
If createElement
resolves to a JavaScript function, there are two places where
the JavaScript conversion happens:
- First all props are converted to JavaScript objects recursively with clj->js.
- Then all reference tags which resolve to Clojure functions are intercepted
so that the props passed to them by
createElement
can be converted back to Clojure maps with js->clj before they are passed to them.
If the Clojure tag functions were not intercepted, you would need to treat the props in your components as JavaScript objects and could not destructure them:
;; Without automatic conversion your components would be rather verbose.
(defn MyButton [js-props]
(jsx>
;; cljsx is expecting the props to be a Clojure map
(<button ... (js->clj js-props)
:className (str "my-button " (.-class js-props)) >)))
;; With automatic conversion, your components should be nice and readable.
(defn MyButton [{:keys [class] :as clj-props}]
(jsx>
(<button ... clj-props
:className (str "my-button " class) >)))
JavaScript Function Detection Pitfalls
In ClojureScript, testing whether whether a function is a JavaScript or Clojure
function is not trivial and not always possible. cljsx
has it's own
cljsx.core/clj-fn?
macro for this purpose. The function returns true
if it
is certain that the object passed to it is a Clojure function, false
if it is
certain that it's a JavaScript function, and nil
if it can't be determined.
Following are cases when cljsx.core/clj-fn?
returns nil
and thus
favours JavaScript conversion:
A function is reassigned to a let binding:
(defn f [])
(let [f' f ; Metadata is lost on reassignment.
;; But fresh assignmet still works.
g (fn [])
h #(identity %)]
[(clj-fn? f')
(clj-fn? g)
(clj-fn? h)])
;; => [nil true true]
A function is passed as an argument to another function:
(defn f [])
(defn g [x]
(clj-fn? x))
(g f)
;; => nil
(clj-fn? (identity f))
;; => nil
A function is returned from another function:
(defn f [])
(clj-fn? ((fn [] f)))
;; => nil
A function is defined or reassigned in the same cljsx
macro where it is used:
(defn f [])
(def g f)
(jsx>
(defn f' [])
(def g' f'))
(jsx>
(defn f'' [])
(def g'' f'')
[(clj-fn? f) ; true
(clj-fn? g) ; true
(clj-fn? f') ; true
(clj-fn? g') ; true
;; Only definitions inside this very jsx> block are indeterminate
(clj-fn? f'') ; nil
(clj-fn? g'') ; nil
])
;; => [true true true true nil nil]
Taking JavaScript Conversion Under Control
There are various ways how you can ensure that your functions called by React will always be either Clojure data structures of JavaScript objects.
Setting a Var's Tag Meta
The cljsx.core/clj-fn?
is consulting the var's metadata :tag
value (among other things), which is lost in all of the cases when the function
returns nil
. You can manually set the tag and thus controll the conversion
of its arguments. If the tag value is js
, or it is not set at all, it will be
treated as a JavaScript function. If the tag is set to any value other than
js
or nil
, it will be treated as a Clojure function.
(defn f [])
(defn ^js f-js [])
(let [^cljs f-clj]
[(clj-fn? f)
(clj-fn? f-js)
(clj-fn f-clj)])
;; => [true false true]
This approach however is rather verbose and it doesn't work in the case when a function is defined inside the same jsx macro call where it is used.
Conversion Decorators
Since the JavaScript conversion favours JavaScript when the result of the
underlying clj-fn?
function is nil
, you should only be concerned about what
is passed to the components you defined and which you have under control.
There are number of utilities which you can use to make sure that your components will always receive their props as a Clojure map or a JavaScript object.
The with-js-args
and with-clj-args
Decorators
The with-js-args
decorator takes a function and returns the same function for
which each argument will be passed trhough clj->js. Conversely the
with-clj-args
function takes a function and returns the same function for
which all arguments will be passed trhough js->clj.
(ns my-react-app.main
(:require [cljsx.core :refer [react> with-clj-args]]
[react :as react]
[react-dom :as react-dom]))
(react>
(defn Component [props]
(js/console.log "Clojure props:" (map? props))
(<h1> "Hello!"))
(def ComponentWithCLJProps (with-clj-args Component))
(react-dom/render
(<div>
(<Component :x >) ; Clojure props: false
(<ComponentWithCLJProps :x >) ; Clojure props: true
(js/document.querySelector "#mount-point"))))
Function Definition Macros
Wrapping every component you write in a decorator is tedious and annoying.
But fear not, cljsx
comes with a bunch of macros which simplify definition of
functions with predictable data format.
The fn-clj
And defn-clj
Macros
The fn-clj
and defn-clj
macros have the same signature and work the same as
their fn
and defn
counterparts, except that the functions they define will
always receive JavaScript arguments converted to Clojure data structures using
js->clj with :keywordize-keys true
.
Functions defined with fn-clj
and defn-clj
work predictably everywhere,
even in the same jsx>
macro where they are defined.
((fn-clj [x y & z] [x y z])
#js{"x" 11} #js{"x" 22} #js{"z" 33})
;; => [{:x 11} {:x 22} ({:z 33})]
;; You can define multiple arities
(def multi-arity-fn (fn-clj ([] "nullary")
([x] ["unary" x])
([x y] ["binary" x y])
([x y & z] ["variadic" x y z])))
(multi-arity-fn)
;; => "nullary"
(multi-arity-fn #js{"x" 11})
;; => ["unary" {:x 11}]
(multi-arity-fn #js{"x" 11} #js{"x" 22})
;; => ["binary" {:x 11} {:x 22}]
(multi-arity-fn #js{"x" 11} #js{"x" 22} #js{"z" 33})
;; => [{:x 11} {:x 22} ({:z 33})]
These macros are especially usefull for defining callbacks for JavaScript libraries like React Router.
(ns shadow-cljs-example.main
(:require ["react" :as react]
["react-dom" :as react-dom]
["react-router-dom" :as rr]
[cljsx.core :refer [defn-clj fn-clj react>]]))
(react>
;; This function will always receive a Clojure map, eventhough
;; its defined in the same react> call where it is used.
(defn-clj Link [{:keys [href active children]}]
(if active
children
(<rr/Link :to href >
children)))
(react-dom/render
(<rr/BrowserRouter>
(<rr/Route>
;; We can destructure the React Router route props, which are passed as a
;; JavaScript object because we are using fn-clj and not just fn.
(fn-clj [{{path :pathname} :location :as x}]
(js/console.log (subs path 1))
(<ul>
(map #(<li :key % >
;; React will pass the props to Link as a JS object,
;; but since Link is defined with def-clj,
;; it will recieve the props as a Clojure map.
(<Link :href %
:active (= % (subs path 1)) >
%))
["foo" "bar" "baz"])))))
(js/document.querySelector "#mount-point")))
The component
and defcomponent
Macros
The component
and defcomponent
macros are just a stripped-out version of the
fn-clj
and defn-clj
macros. The only difference is that they define a
unary function with props as the only argument, which can be a map, nil
or
a plain JavaScript object, in which case, similarly as with def-clj
the
functions will receive the argument converted using js->clj with
:keywordize-keys true
.
The component
and defcomponent
signatures are the same as the signatures of
fn-clj
and defn-clj
(or fn and defn) respectively with these rather
cosmetic differences:
- Since both define unary functions there's no point in supporting multiple arities.
- For the same reason, we don't need the argument vector, but only the single argument symbol or a destructuring map.
- In
defcomponent
, metadata can only be set with the^
reader macro in front of the component name and not with the metadata map before the argument vector as is possible indefn-clj
and defn. - Both
fn-clj
and fn accept an optional name parameter i.e. a symbol in of the argument vector e.g(fn my-name [x] fn-body)
. In thecomponent
macro, the name must be a qouted symbol so that it can be distinguished from the possible prop symbol e.g.(component 'my-name props fn-body)
.
(defcomponent MyComponent props
(map? props))
;; The above statement is equivalent to this.
;; The only difference is the missing square brackets of the argument vector.
(defn-clj MyComponent' [props]
(map? props))
(MyComponent {:a "A"}) ; true
;; Even if you call it with a JS object, it still receives a Clojure map
(MyComponent #js{:a "A"}) ; true
(react>
;; Even if you define the component in the same JSX macro call as it is used,
;; it will still predictably receive its props as a Clojure map.
(defcomponent ^:before-load StyledButton
"A button which adds the .my-button class to its class name list."
;; Destructuring stil works on the single argument.
{:keys [className] :or {className ""} :as props}
;; Pre and post conditions work the same as in defn
{:pre [(string? className)]}
(<button ... props
:className (str className " my-button") >))
(react-dom-server/renderToStaticMarkup
;; The component will receive a Clojure map, eve though
;; React passes the props to it as a JavaScript object.
(<StyledButton :className "foo" >
"Click me!")))
;; => "<button class=\"foo my-button\">Click me!</button>"
Example
And finally an example of a simple application which uses Material-UI and
React Router written in JSX, cljsx
and reagent.
JavaScript With JSX
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter, Link, Route } from 'react-router-dom'
import { Avatar, Card, CardContent, CardHeader, IconButton } from '@material-ui/core'
import * as icons from '@material-ui/icons'
const IconButtonLink = props =>
<IconButton {...props} component={Link} />
const App = () =>
<BrowserRouter>
<Route>
{({location: {pathname: path}}) => {
const activeIconName = path.slice(1)
const ActiveIcon = icons[activeIconName]
const title = ActiveIcon ? activeIconName : "Click the icons"
return (
<Card>
<CardHeader
title={title}
avatar={ActiveIcon && <Avatar><ActiveIcon/></Avatar>}/>
<CardContent>
{Object.keys(icons)
.filter(iconName => iconName.endsWith('TwoTone'))
.map(iconName => {
const Icon = icons[iconName]
const color = iconName === activeIconName
? "secondary"
: "default"
return (
<IconButtonLink key={iconName} to={iconName} color={color} >
<Icon/>
</IconButtonLink>
)
})}
</CardContent>
</Card>
)
}}
</Route>
</BrowserRouter>
ReactDOM.render(
<App/>,
document.querySelector('#mount-point'),
)
ClojureScript With CLJSX
(ns shadow-cljs-example.main
(:require ["react" :as react]
["react-dom" :as react-dom]
["react-router-dom" :as rr]
["@material-ui/core" :as mui]
["@material-ui/icons" :as mui-icons]
[cljsx.core :refer [defcomponent fn-clj react>]]))
(react>
(defcomponent IconButtonLink props
(<mui/IconButton ... props
:component rr/Link >))
(defcomponent App _
(<rr/BrowserRouter>
(<rr/Route>
(fn-clj [{{path :pathname} :location}]
(let [active-icon-name (subs path 1)
^js ActiveIcon (aget mui-icons active-icon-name)]
(<mui/Card>
(<mui/CardHeader :title (if ActiveIcon
active-icon-name
"Click the icons")
:avatar (when ActiveIcon
(<mui/Avatar>
(<ActiveIcon>))) >)
(<mui/CardContent>
(for [icon-name (js/Object.keys mui-icons)
:when (.endsWith icon-name "TwoTone")
:let [^js Icon (aget mui-icons icon-name)]]
(<IconButtonLink :key icon-name
:to icon-name
:color (if (= icon-name active-icon-name)
"secondary"
"default") >
(<Icon>))))))))))
(react-dom/render
(<App>)
(js/document.querySelector "#mount-point")))
ClojureScript With Reagent
(ns shadow-cljs-example.main
(:require ["react-router-dom" :as rr]
["@material-ui/core" :as mui]
["@material-ui/icons" :as mui-icons]
[reagent.core :as r]))
(defn icon-button-link [props & children]
(into [:> mui/IconButton (merge props {:component rr/Link})]
children))
(defn app []
[:> rr/BrowserRouter
[:> rr/Route
(fn [js-route-props]
(let [path (-> js-route-props .-location .-pathname)
active-icon-name (subs path 1)
ActiveIcon (aget mui-icons active-icon-name)]
(r/as-element
[:> mui/Card
[:> mui/CardHeader {:title (if ActiveIcon
active-icon-name
"Click the icons")
:avatar (r/as-element
[:> mui/Avatar
(when ActiveIcon
[:> mui/Avatar
[:> ActiveIcon]])])}]
[:> mui/CardContent
(for [icon-name (js/Object.keys icons)
:when (.endsWith icon-name "TwoTone")
:let [Icon (aget mui-icons icon-name)]]
^{:key icon-name}
[icon-button-link {:to icon-name
:color (if (= icon-name active-icon-name)
"secondary"
"default")}
[:> Icon]])]])))]])
(r/render
[app]
(js/document.querySelector "#mount-point"))