Getting Started With Rum
6 Apr, 2018
What is 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.
It hides away some React complexity parts, and looks more Clojure'ish.
How does it work
Let's take a look on a simple RUM component.
(rum/defc app [text]
[:.app text])
Rum uses Hiccup-like syntax for defining markup:
[tag-n-selector attrs? & children]
So app
component html could look like:
[:div.app { :id "app" } [:span text]]
Let's have a deeper look on rum/defc
macros. It does a couple of things before you get your React component back.
Render function
First, it's being translated into the following anonymous function:
(fn [text]
(sablono/compile-html
[:div.app text]))
Then sablono translates the vector into React.createComponent
:
(fn [text]
(js/React.createComponent "div" #js { "class" "app" } text))
React class
After expanding a render function rum/defc
creates a react class:
(js/React.createClass
#js { "displayName" "app"
"getInitialState" (() => props)
"render" (fn [] (apply <render-fn> (:rum/args state))) })
Having getInitialState
part is more like a trick. React does not allow you to pass the props straight to the render function. So we have to save them in the state and use the state from inside the render function.
Factory constructor
Finally rum/defc
creates a real function which returns you a react element.
(defn app [text]
(js/React.createElement <class> { :rum/args [text] }))
You can pass your text
prop to this function and mount it into a container with rum/mount
.
The source code of this macros is available on github.com/tonsky/rum/blob/gh-pages/src/rum/core.clj.
Rum state
It is different from the react state. It uses React state in the end, but have nothing in common in the implementation.
It uses a Clojure's map with several properties:
{:rum/args ["hello"] ;; passed to the component as props
:rum/id 1010 ;; internal information
:rum/react-component ...} ;; an actual react component (js object)
It is accesible in every method of Rum.
There is a special form of rum/defc
macros rum/defcs
which means def component with state
.
(rum/defcs app [state text]
[:.app (pr-str (:rum/args state))])
app
is still one argument function, we don't need to pass the state there.
(rum/mount (app "Hello") (js/document.getElementById "root"))
Component lifecycle
Rum components go with the same idea of lifecycle as react components. Here are the available methods:
{:init ;; state, props ⇒ state
:will-mount ;; state ⇒ state
:before-render ;; state ⇒ state
:wrap-render ;; render-fn ⇒ render-fn
:render ;; state ⇒ [pseudo-dom state]
:did-catch ;; state, err, info ⇒ 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
Let's try to hook into did-mount
method. As we see from the docs, it's a function (state) => state
.
We can access it using a mixin with the proper key:
(rum/defcs app < {:did-mount
(fn [state]
(js/console.log (:rum/react-component state))
state)}
[state text]
[:div (pr-str state) text])
It should print a react component object into your browser's console.
Now let's try to update the state.
(rum/defcs app < {:will-mount
(fn [state]
(assoc state ::counter 1))}
[state text]
[:div (pr-str state) text])
To be able to see the updated state, we need to change did-mount
to will-mount
as it's executed before component being mounted.
Also, better use namespaced keys:
{::counter 1} ;; not {:counter 1}
request-render
Rum uses requestAnimationFrame
to batch and debounce component update calls. You can call rum/request-render
to schedule react component's update at next frame.
For example, let's update the component every second:
(rum/defc app < {:did-mount
(fn [state]
(let [comp (:rum/react-component state)
interval (js/setInterval #(rum/request-render comp) 1000)]
(assoc state ::interval interval)))
:will-unmount
(fn [state]
(js/clearInterval (::interval state))
(dissoc state ::interval))}
[]
[:div (.toISOString (js/Date.))])
Conclusion
Rum is an awesome small and simple library. You should definitely check out the sources. The documentation is great and most of the things I talked about in this article are fully covered in the docs.
References
- Rum workshop by tonsky
- Rum repository on github