ClojureScript's Rum with Preact.js instead of React
Prum (Pale Rum) is a fork of Rum library that uses Preact.js as an underlying UI rendering facility
[org.roman01la/prum "0.10.8-11"]
Similar to Rum, Prum is using Hiccup compiler to transform Hiccup into JavaScript calls to Preact's API. However Hiccup interpretation is disabled in Prum, since it adds performance overhead.
Due to this restrictions are the following:
prum.compiler/html
macro when returning Hiccup from a function[:> ReactSelect {:value "value" :options options}]
;; (preact/createElement ReactSelect #js {:value value :options options})
Preact supports only function refs. However string refs is still useful and easier to use in ClojureScript. To handle this properly there's a new helper function rum.core/use-ref
(rum/defc input []
[:input {}])
(rum/defcc form <
{:after-render
(fn [state]
(rum/ref state :btn) ;; returns DOM node of the element
(rum/ref state :input) ;; returns component
(rum/ref-node state :input) ;; returns top-level DOM node of the component
state)}
[comp]
[:form {}
(rum/with-ref (input) (rum/use-ref comp :input))
[:button {:ref (rum/use-ref comp :btn)} "text"]])
Preact components doesn't implement contextTypes
and childContextTypes
as in React. This means that in Prum there's no need to declare :contextTypes
and :childContextTypes
in :class-properties
mixin.
Also there's a helper function to read from context rum.core/context
.
(rum/defcc rum-context-comp [comp]
[:span
{:style {:color (rum/context comp :color)}}
"Child component uses context to set font color."])
(rum/defc context <
{:child-context (fn [state] {:color @core/*color})}
[]
[:div {}
[:div {} "Root component implicitly passes data to descendants."]
(rum-context-comp)])
When re-rendering from the root, by default Preact appends to root DOM node. To re-render properly rum.core/mount
accepts optional third argument, which is the root node to replace.
(def root (rum/mount (app) dom-node)) ;; returns root DOM node
(rum/mount (app) dom-node root) ;; pass in the root node to render and replace
When rendering a list of values, a collection of elements should not be a vector.
[:ul {} (mapv render-item items)] ;; this is wrong
[:ul {} (map render-item items)] ;; this is ok
[:ul {} [[:li {} "#1"] [:li {} "#2"]]] ;; this is wrong
[:ul {} '([:li {} "#1"] [:li {} "#2"])] ;; this is ok
:css
attributeProvided by Clojure Style Sheets library.
[:button {:css {:font-size "12px"}}]
Preact use native (in-browser) event system instead of Synthetic Events system as in React, thus it doesn't change behaviour of DOM events. However to stay compatible with Rum/React, Prum translates :on-change
handlers into :on-input
as React does.
Below is original unmodified documentation of Rum
Rum is a client/server library for HTML UI. In ClojureScript, it works as React wrapper, in Clojure, it is a static HTML generator.
Simple semantics: Rum is arguably smaller, simpler and more straightforward than React itself.
Decomplected: Rum is a library, not a framework. Use only the parts you need, throw away or replace what you don’t need, combine different approaches in a single app, or even combine Rum with other frameworks.
No enforced state model: Unlike Om, Reagent or Quiescent, Rum does not dictate where to keep your state. Instead, it works well with any storage: persistent data structures, atoms, DataScript, JavaScript objects, localStorage or any custom solution you can think of.
Extensible: the API is stable and explicitly defined, including the API between Rum internals. It lets you build custom behaviours that change components in significant ways.
Minimal codebase: You can become a Rum expert just by reading its source code (~900 lines).
Rum:
Add to project.clj: [rum "0.10.8"]
Use rum.core/defc
(short for “define component”) to define a function that returns component markup:
(require [rum.core :as rum])
(rum/defc label [text]
[:div {:class "label"} text])
Rum uses Hiccup-like syntax for defining markup:
[<tag-n-selector> <attrs>? <children>*]
<tag-n-selector>
defines a tag, its id and classes:
:span
:span#id
:span.class
:span#id.class
:span.class.class2
By default, if you omit the tag, div
is assumed:
:#id === :div#id
:.class === :div.class
<attrs>
is an optional map of attributes:
:allow-full-screen
for allowFullScreen
):id
and :class
there as well:class
can be a string or a sequence of strings:style
, if needed, must be a map with kebab-case keywords[:input { :type "text"
:allow-full-screen true
:id "comment"
:class ["input_active" "input_error"]
:style { :background-color "#EEE"
:margin-left 42 }
:on-change (fn [e]
(js/alert (.. e -target -value))) }]
<children>
is a zero, one or many elements (strings or nested tags) with the same syntax:
[:div {} "Text"] ;; tag, attrs, nested text
[:div {} [:span]] ;; tag, attrs, nested tag
[:div "Text"] ;; omitted attrs
[:div "A" [:em "B"] "C"] ;; 3 children, mix of text and tags
Children can include lists or sequences which will be flattened:
[:div (list [:i "A"] [:b "B"])] === [:div [:i "A"] [:b "B"]]
By default all text nodes are escaped. To embed an unescaped string into a tag, add the :dangerouslySetInnerHTML
attribute and omit children:
[:div { :dangerouslySetInnerHTML {:__html "<span></span>"}}]
Given this code:
(require [rum.core :as rum])
(rum/defc repeat-label [n text]
[:div (repeat n [:.label text])])
First, we need to create a component instance by calling its function:
(repeat-label 5 "abc")
Then we need to pass that instance to (rum.core/mount comp dom-node)
:
(rum/mount (repeat-label 5 "abc") js/document.body)
And we will get this result:
<body>
<div>
<div class="label">abc</div>
<div class="label">abc</div>
<div class="label">abc</div>
<div class="label">abc</div>
<div class="label">abc</div>
</div>
</body>
Usually, mount
is used just once in an app lifecycle to mount the top of your component tree to a page. After that, for a dynamic applications, you should either update your components or rely on them to update themselves.
The simplest way to update your app is to mount it again:
(rum/defc timer []
[:div (.toISOString (js/Date.))])
(rum/mount (timer) js/document.body)
(js/setInterval
#(rum/mount (timer) js/document.body)
1000)
Rum offers mixins as a way to hook into a component’s lifecycle and extend its capabilities or change its behaviour.
One very common use-case is for a component to update when some reference changes. Rum has a rum.core/reactive
mixin just for that:
(def count (atom 0))
(rum/defc counter < rum/reactive []
[:div { :on-click (fn [_] (swap! count inc)) }
"Clicks: " (rum/react count)])
(rum/mount (counter) js/document.body)
Two things are happening here:
rum.core/reactive
mixin to the component.rum.core/react
instead of deref
in the component body.This will set up a watch on the count
atom and will automatically call rum.core/request-render
on the component each time the atom changes.
Sometimes you need to keep track of some mutable data just inside a component and nowhere else. Rum provides the rum.core/local
mixin. It’s a little trickier to use, so hold on:
rum.core/local
creates a mixin that will put an atom into the component’s state.rum.core/defcs
is used instead of rum.core/defc
. It allows you to get hold of the components’s state in the render function (it will be passed as a first argument).deref
/swap!
/reset!
it as usual.In practice, it’s quite convenient to use:
(rum/defcs stateful < (rum/local 0 ::key)
[state label]
(let [local-atom (::key state)]
[:div { :on-click (fn [_] (swap! local-atom inc)) }
label ": " @local-atom]))
(rum/mount (stateful "Click count") js/document.body)
If your component accepts only immutable data structures as arguments, it may be a good idea to add the rum.core/static
mixin:
(rum/defc label < rum/static [n text]
[:.label (repeat n text)])
rum.core/static
will check if the arguments of a component’s constructor have changed (using Clojure’s -equiv
semantic), and if they are the same, avoid re-rendering.
(rum/mount (label 1 "abc") body)
(rum/mount (label 1 "abc") body) ;; render won’t be called
(rum/mount (label 1 "xyz") body) ;; this will cause a re-render
(rum/mount (label 1 "xyz") body) ;; this won’t
Note that this is not enabled by default because a) comparisons can be expensive, and b) things will go wrong if you pass a mutable reference as an argument.
Many applications have very specific requirements and custom optimization opportunities, so odds are you’ll be writing your own mixins.
Let’s see what a Rum component really is. Each Rum component has:
For example, if we have this component defined:
(rum/defc input [label value]
[:label label ": "
[:input { :value value }]])
(input "Your name" "")
It will have the following state:
{ :rum/args ["Your name" ""]
:rum/react-component <react-component> }
You can read the internal state by using the rum.core/defcs
(short for “define component [and pass] state”) macro instead of rum.core/defc
. It will pass state
to the render function as the first argument:
(rum/defcs label [state label value]
[:div "My args:" (pr-str (:rum/args state))])
(label "A" 3) ;; => <div>My args: ["A" 3]</div>
The internal state cannot be directly manipulated, except at certain stages of a component’s lifecycle. Mixins are functions that are invoked at these stages to give you and opportunity to modify the state and/or do side effects to the world.
The following mixin will record the component’s mount time:
(rum/defcs time-label < { :will-mount (fn [state]
(assoc state ::time (js/Date.))) }
[state label]
[:div label ": " (str (::time state))])
As you can see, :will-mount
is a function from state
to state
. It gives you a chance to populate, clean or modify state map the moment before the component has been mounted.
Another useful thing you can do in a mixin is to decide when to update a component. If you can get ahold of React component (notice that that’s different from Rum component, unfortunately; sorry), you can call rum.core/request-render
to schedule this component’s update at next frame (Rum uses requestAnimationFrame
to batch and debounce component update calls). To get React component, just look up :rum/react-component
key in a state.
This mixin will update a component each second:
(def periodic-update-mixin
{ :did-mount (fn [state]
(let [comp (:rum/react-component state)
callback #(rum/request-render comp)
interval (js/setInterval callback 1000)]
(assoc state ::interval interval)))
:will-unmount (fn [state]
(js/clearInterval (::interval state))
(dissoc state ::interval)) })
(rum/defc timer < periodic-update-mixin []
[:div (.toISOString (js/Date.))])
(rum/mount (timer) js/document.body)
Here’s a full list of callbacks you can define in a mixin:
{ :init ;; state, props ⇒ state
:will-mount ;; state ⇒ state
:before-render ;; state ⇒ state
:wrap-render ;; render-fn ⇒ render-fn
:render ;; state ⇒ [pseudo-dom state]
:did-mount ;; state ⇒ state
:after-render ;; state ⇒ state
:did-remount ;; old-state, state ⇒ state
:should-update ;; old-state, state ⇒ boolean
:will-update ;; state ⇒ state
:did-update ;; state ⇒ state
:will-unmount } ;; state ⇒ state
Each component can have any number of mixins:
(rum/defcs component < rum/static
rum/reactive
(rum/local 0 ::count)
(rum/local "" ::text)
[state label]
(let [count-atom (::count state)
text-atom (::text state)]
[:div])
One gotcha: don’t forget to return state
from the mixin functions. If you’re using them for side-effects only, just return an unmodified state
.
Since Rum relies a lot at components being able to efficiently update themselves in reaction to events, it includes two facilities to build architectures around Atoms and watchers.
Cursors
If you have a complex state and need a component to interact with only a part of it, create a cursor using (rum.core/cursor-in ref path)
. Given atom with deep nested value and path inside it, cursor-in
will create an atom-like structure that can be used separately from main atom, but will sync changes both ways:
(def state (atom { :color "#cc3333"
:user { :name "Ivan" } }))
(def user-name (rum/cursor-in state [:user :name]))
@user-name ;; => "Ivan"
(reset! user-name "Oleg") ;; => "Oleg"
@state ;; => { :color "#cc3333"
;; :user { :name "Oleg" } }
Cursors implement IAtom
and IWatchable
and interface-wise are drop-in replacement for regular atoms. They work well with rum/reactive
and rum/react
too.
Derived atoms
Use derived atoms to create “chains” and acyclic graphs of dependent atoms. derived-atom
will:
f
, passing N dereferenced values of source refsreset!
result of f
to the sink atom (def *a (atom 0))
(def *b (atom 1))
(def *x (derived-atom [*a *b] ::key
(fn [a b]
(str a \":\" b))))
(type *x) ;; => clojure.lang.Atom
@*x ;; => 0:1
(swap! *a inc)
@*x ;; => 1:1
(reset! *b 7)
@*x ;; => 1:7
Derived atoms are like cursors, but can “depend on” multiple references and won’t sync changes back to the source if you try to update derived atom (don’t).
Native React component
You can access the raw React component by reading the state’s :rum/react-component
attribute:
{ :did-mount (fn [state]
(let [comp (:rum/react-component state)
dom-node (js/ReactDOM.findDOMNode comp)]
(set! (.-width (.-style dom-node)) "100px"))
state) }
React keys and refs
There’re three ways to specify React keys:
[:div { :key "x" }]
with-key
:(rum/defc my-component [str]
...)
(rum/with-key (my-component "args") "x")
:key-fn
in a mixin to calculate key based on args at component creation time:(rum/defc my-component < { :key-fn (fn [x y z]
(str x "-" y "-" z)) }
[x y z]
...)
(my-component 1 2 3) ;; => key == "1-2-3"
:key-fn
must accept same arguments your render function does.
Refs work the same way as options 1 and 2 for keys work:
[:div { :ref "x" }]
(rum/with-ref (my-component) "x")
Accessing DOM
There’re couple of helpers that will, given state map, find stuff in it for you:
(rum/dom-node state) ;; => top-level DOM node
(rum/ref state "x") ;; => ref-ed React component
(rum/ref-node state "x") ;; => top-level DOM node of ref-ed React component
Custom class properties
To define arbitrary properties and methods on a component class, specify a :class-properties
map in a mixin:
(rum/defc comp < { :class-properties { ... } }
[:div]))
React context
To define child context, specify a :child-context
function taking state and returning context map in a mixin:
(rum/defc theme < { :child-context
(fn [state]
(let [[color] (:rum/args state)]
{ :color color }))
:class-properties
{ :childContextTypes {:color js/React.PropTypes.string} } }
[color child]
child)
If used from clj/cljc, Rum works as a traditional template engine à la Hiccup:
rum.core
as usual.rum/defc
or other macros as usual.rum/render-html
to render into a string.(require '[rum.core :as rum])
(rum/defc my-comp [s]
[:div s])
;; on a server
(rum/render-html (my-comp "hello"))
;; => "<div data-reactroot=\"\" data-reactid=\"1\" data-react-checksum=\"-857140882\">hello</div>"
;; on a client
(rum/mount (my-comp "hello") js/document.body)
Use rum/render-static-markup
if you’re not planning to connect your page with React later:
(rum/render-static-markup (my-comp "hello")) ;; => <div>hello</div>
Rum server-side rendering does not use React or Sablono, it runs completely in JVM, without involving JavaScript at any stage.
As of [rum "0.8.3"]
and [hiccup "1.0.5"]
, Rum is ~3× times faster than Hiccup.
Server-side components do not have full lifecycle support, but :init
and :will-mount
from mixins would be called at the component’s construction time.
aria-*
values as strings (thx r0man, PR #114)<
is misplaced in defc
(thx Martin Klepsch, issue #88, PR #90)rum.core/react
is used without rum.core/reactive
(thx Martin Klepsch, issue #82, PR #87):pre
and :post
checks in rum.core/defc
(thx Martin Klepsch, PR #81)displayName
in 0.10.0:before-render
and :will-update
weren’t called on subsequent rendersrum.core/state
public again:before-render
should be called on server-side rendering too (thx Alexander Solovyov, PR #79)A big cleanup/optmization/goodies release with a lot breaking changes. Read carefully!
cursor
got renamed to cursor-in
. New cursor
method added that takes single key (as everywhere in Clojure)rum/mount
returns nil
(because you shouldn’t rely on return value of ReactDOM.render):transfer-state
is gone. All of component’s state is now transferred by default. If you still need to do something fancy on componentWillReceiveProps
, new callback is called :did-remount
callbackcursored
and cursored-watch
mixins. They felt too unnatural to userum/with-props
(deprecated since 0.3.0). Use rum/with-key
and rum/with-ref
instead:did-mount
(obviously, that was a mistake):rum/id
is gone. If you need an unique id per component, allocate one in :init
as store it in state under namespaced keyWhen upgrading to 0.10.0, check this migration checklist:
rum/cursor
calls to rum/cursor-in
:transfer-state
mixins.
(fn [old new] (assoc new ::key (::key old)))
, just delete them.:did-remount
rum/mount
return value. If yes, find another way to obtain component (e.g. via ref
, defcc
etc)rum/with-props
with rum/with-key
, rum/with-ref
or :key-fn
:did-mount
in server-side renderingNow for the good stuff:
alter-meta!
etc:key-fn
to mixins. That function will be called before element creation, with same arguments as render fn, and its return value will be used as a key on that element:before-render
(triggered at componentWillMount
and componentWillUpdate
) and :after-render
(componentDidMount
and componentDidUpdate
) callbackrum/ref
and rum/ref-node
helpers, returning backing component and DOM nodedom-node
, unmount
, request-render
etc). Their implementation just throws an exception. This is to help you write less conditional directives in e.g. :did-mount
or :will-unmount
mixins. They will never be called, but won’t stop code from compiling either.And couple of optimizations:
defc
with no mixins, it’ll be automatically compiled to stateless component)requestAnimationFrame
in a single chunkrender->mixin
, args->state
, element
and ctor->class
rum.core/derived-atom
, a function that let you build reactive chains and directed acyclic graphs of dependent atoms. E.g. you want *c
to always contain a value of *a
plus a value of *b
and update whenever any of them changesrum.core/dom-node
helper that takes state and finds corresponding top DOM node of a component. Can be called in mixins after initial render onlywith-key
on nil-returning component in server rendering (thx Alexander Solovyov, PR #73):allow-full-screen
, :allowFullScreen
and "allowFullScreen"
would be normalized to allowfullscreen
. As a result, you have to face three problems:
Starting with 0.9.0, Rum will adopt “There’s Only One Way To Do It” policy. All attributes MUST be specified as kebab-cased keywords:
Attribute | What to use | What not to use |
---|---|---|
class | :class |
:class-name :className |
for | :for |
:html-for :htmlFor |
unescaped innerHTML | :dangerouslySetInnerHTML { :__html { "..." }} |
|
uncontrolled value | :default-value |
:defaultValue |
uncontrolled checked | :default-checked |
:defaultChecked |
itemid, classid | :item-id , :class-id |
:itemID :itemId :itemid |
xml:lang etc | :xml-lang |
:xml/lang :xmlLang "xml:lang" |
xlink:href etc | :xlink-href |
:xlink/href :xlinkHref "xlink:href" |
xmlns | not supported |
To migrate to 0.9.0 from earlier versions, just do search-and-replace for non-standard variants and replace them with recommended ones.
rum/render-static-markup
call for pure HTML templating. Use it if you’re not planning to connect your page with React laterrum/def*
macros now correctly retain metadata that already exists on a symbol (thx aJchemist, PR #62)rum.core/unmount
function (thx emnh, issue #61):arglists
metadata on vars defined by rum/def*
macros (thx aJchemist, PR #60)rum/render-html
(thx Alexander Solovyov):class-properties
to define arbitrary properties on a React class (thx Karanbir Toor, PR #44):child-context-types
and :context-types
. Use { :class-properties { :childContextTypes ..., :contextTypes ... } }
instead.setTimeout
in global scope instead of in window (thx Alexander Solovyov, PR #43)rum.core
under any alias you want (thx Stuart Hinson, PR #42)rum
to rum.core
to supress CLJS warningschild-context
, child-context-types
, context-types
(thx Karanbir Toor, PR #37)defcc
macro for when you only need React component, not the whole Rum state:rum/state
) was moved from props
to state
. It doesn’t change a thing if you were using Rum API only, but might break something if you were relaying on internal detailsrum/with-props
macro, use rum/with-key
or rum/with-ref
fns insteadlocal
mixinwill-update
and did-update
lifecycle methods added (thx Andrey Vasenin, pull request #18)defc/defcs
will have displayName
defined (thx Ivan Dubrov, pull request #16)requestAnimationFrame
when used in headless environment (thx @whodidthis, pull request #14)(:require rum)
(defc name < mixin1 mixin2 [args] body...)
defcs
macro that adds additional first argument to render function: state
key
and ref
to rum components via with-props
.forceUpdate
unmounted elementsreactive
mixin:should-update
from reactive
, it now will be re-rendered if re-created by top-level elementreactive
with static
to avoid re-rendering if component is being recreated with the same argsRum was build on inspiration from Quiescent, Om and Reagent.
All heavy lifting done by React, Ŝablono and ClojureScript.
Copyright © 2014–2016 Nikita Prokopov
Licensed under Eclipse Public License (see LICENSE).