Getting Started With Rum

Apr 6, 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 defenately 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