Rum goals:
- Be ClojureScript-friendly
- Provide clean, simple and straightforward API (with simpler semantics than React’s own)
- Allow for mixing different kinds of components in a single app
- Allow for building new kinds of components easily
- Stay minimal
Rum is not a framework that tells you how your components should work. Instead, it’s a library that gives you the tools, so you can build components that fits your needs best.
All ClojureScript frameworks: Om, Reagent and even Quiescent came with built-in component behaviour model. They do not allow to change that without rewriting internals. Rum was designed to specifically address that problem. Rum doesn’t sell you one true component model. Instead, contract on custom component building (mixins) is also considered to be part of API in Rum.
Rum has two levels of API. On lower level we have tools to build your own component behaviours: low-level details are well-defined and open for extensions. Thanks to that Rum is more customizable, integration with third-party models is simpler (you use storage/data model you want and write component to support that, unlike other solutions which dictate how to store app state), and you can mix different kinds of components in one app.
On higher level, Rum comes with already-built types of components which emulate behavior found in Om, Reagent and Quiescent. They were built using the same public API any Rum user can use. No internals hacking. I think it means abstraction is good enough and decomplection was made in the right place. I’m also very proud they take about 10-30 lines of code each.
Rum idea is not to lock you down to a single storage model. Sometimes your dataflow is trickier that just atoms, e.g. you need components to react to DataScript events, core.async channels, ajax/websocket/webworker callbacks. In that case Rum provides well-defined API and set of basic building blocks to write components you need to.
Rum provides basic tools that every React app eventually need:
requestAnimationFrame
-based render queue- component id generator
sablono
-based syntax for generating markup- couple of wrapper functions like
mount
,build-class
,element
etc.
- Add
[rum "0.3.0"]
to dependencies (require 'rum)
.
Simplest example defines component, instantiates it and mounts it on a page:
(ns example
(:require rum))
(rum/defc label [n text]
[:.label (repeat n text)])
(rum/mount (label 5 "abc") (.-body js/document))
For more examples, see examples/examples.cljs. Live version of examples is here
Note: To suppress warning about single-segmented namespace, add this:
:compiler {
{:warnings {:single-segment-namespace false}}
}
- Norbert Wójtowicz talk at Lambda Days 2015 where he explains benefits of web development with ClojureScript and React, and how Rum emulates all main ClojureScript frameworks
- DataScript Chat sample app: github.com/tonsky/datascript-chat
- DataScript ToDo sample app: github.com/tonsky/datascript-todo
- DataScript Menu example: github.com/tonsky/datascript-menu
- PartsBox.io, inventory management
Rum provides defc
macro (short from “define component”):
(rum/defc name doc-string? [< mixins+]? [params*] render-body+)
defc
defines top-level function that accepts argvec
and returns React element that renders as specified in render-body
.
Behind the scenes, defc
does couple of things:
- Creates render function by wrapping
render-body
with implicitdo
and then withsablono.core/html
macro - Builds React class from that render function and provided mixins
- Using that class, generates constructor fn [params]->ReactElement
- Defines a top-level var
name
and puts constructor fn there
When called, name
function will create new React element from built React class and pass through argvec
so it’ll be available inside render-body
To mount component, use rum/mount
:
(rum/mount element dom-node)
mount
returns mounted component. It’s safe to call it multiple times over same arguments.
Note that mount
will not make component auto-updatable. It’s up to your code (or mixins) to make it update when you need it. Two common idioms are to mount it again:
(add-watch state :render
(fn [_ _ _ _]
(rum/mount element node)))
or call request-render
function:
(let [component (rum/mount element dom-node)]
(add-watch state :render
(fn [_ _ _ _]
(rum/request-render component))))
request-render
does not execute rendering immediately, instead, it will place your component to render queue and re-render on requestAnimationFrame
callback. request-render
is preferable way to refresh component.
Rum comes with a couple of mixins which emulate behaviors known from quiescent
, om
and reagent
. Developing your own mixin is also very simple.
rum/static
will check if arguments of a component constructor have changed (with Clojure’s -equiv
semantic), and if they are the same, avoid re-rendering.
(rum/defc label < rum/static [n text]
[:.label (repeat n text)])
(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/local
creates an atom that can be used as per-component local state. When you swap!
or reset!
this atom, component will be re-rendered automatically. Atom can be found in state under :rum/local
key:
(rum/defcs stateful < (rum/local 0) [state title]
(let [local (:rum/local state)]
[:div
{:on-click (fn [_] (swap! local inc))}
title ": " @local]))
(rum/mount (stateful "Clicks count") js/document.body)
Note that we used defcs
instead of defc
to get state as first argument to render
. Also note that rum/local
is not a mixin value, instead, it’s a function, generator-like: it takes initial value and returns mixin.
rum/reactive
will create “reactive” component that will track references used inside render
function and auto-update when values of these references change.
(def color (atom "#cc3333"))
(def text (atom "Hello"))
(rum/defc label < rum/reactive []
[:.label {:style {:color (rum/react color)}}
(rum/react text)])
(rum/mount (label) (.-body js/document))
(reset! text "Good bye") ;; will cause re-rendering
(reset! color "#000") ;; and another one
rum/react
function used in this example works as deref
, and additionally adds watch on that reference.
Finally, rum/cursored
is a mixin that will track changes in references passed as arguments:
(rum/defc label < rum/cursored [color text]
[:.label {:style {:color @color}} @text])
Note that cursored
mixin creates passive component: it will not react to any changes in references by itself, and will only compare arguments when re-created by its parent. Additional rum/cursored-watch
mixin will add watches on every IWatchable
in arguments list:
(rum/defc body < rum/cursored rum/cursored-watch [color text]
(label color text))
(rum/mount (body color text) (.-body js/document))
;; will cause re-rendering of body and label
(reset! text "Good bye")
;; and another one
(reset! color "#000")
Rum also provides cursors, an abstraction that provides atom-like interface to subtrees inside an atom:
(def state (atom {:color "#cc3333"
:label1 "Hello"
:label2 "Goodbye"}))
(rum/defc label < rum/cursored [color text]
[:.label {:style {:color @color}} @text])
(rum/defc body < rum/cursored rum/cursored-watch [state]
[:div
(label (rum/cursor state [:color]) (rum/cursor state [:label1]))
(label (rum/cursor state [:color]) (rum/cursor state [:label2]))])
(rum/mount (body state) (.-body js/document))
;; will cause re-rendering of second label only
(swap! state assoc :label2 "Good bye")
;; both will be re-rendered
(swap! state assoc :color "#000")
;; cursors can be swapped and reseted just like atoms
(reset! (rum/cursor state [:label1]) "Hi")
Cursors implement IAtom
and IWatchable
and interface-wise are drop-in replacement for regular atoms. They can be used with reactive
components as well.
Beauty of Rum approach is that you can combine multiple mixins inside single component and you can use completely different classes around the tree. You can have top-level reactive
component, couple of nested static
ones, then a component which updates every second, and inside it a cursored
one. Decomplected, powerful, simple.
Rum defines classes and components. Internally they are React’s classes and components.
Each component in Rum has state associated with it. State is just a CLJS map with:
:rum/react-component
— link to React component/element object:rum/id
— unique component id- everything mixins are using for their internal bookkeeping
- anything you’ve put there (feel free to populate state with your own stuff!)
Reference to current state is stored as volatile!
boxed value at state[":rum/state"]
.
Effectively state is mutable, but components do not change volatile reference directly,
instead all lifecycle functions accept and return state value.
Classes define component behavior, including render function. Class is built from multiple mixins.
Mixins are basic building blocks for designing new components behaviors in Rum. Each mixin is just a map of one or more of following functions:
{ :init ;; state, props ⇒ state
:will-mount ;; state ⇒ state
:did-mount ;; state ⇒ state
:transfer-state ;; old-state, state ⇒ state
:should-update ;; old-state, state ⇒ boolean
:will-update ;; state ⇒ state
:render ;; state ⇒ [pseudo-dom state]
:wrap-render ;; render-fn ⇒ render-fn
:did-update ;; state ⇒ state
:will-unmount ;; state ⇒ state
:child-context ;; state ⇒ child-context }
Additionaly, mixin can specify following maps:
{ :child-context-types { ... }
:context-types { ... } }
Imagine a class built from N mixins. When lifecycle event happens in React (e.g. componentDidMount
), all :did-mount
functions from first mixin to last will be invoked one after another, threading current state value through them. State returned from last :did-mount
mixin will be stored in volatile state reference by Rum. Similarly, context
maps from multiple mixins are combined into one map.
Rendering is modeled differently. There must be single :render
function that accepts state and return 2-vector of dom and new state. If mixin wants to modify render behavior, it should provide :wrap-render
fn that accepts render function and returns modified render function (similar to ring middlewares). :wrap-render
fns are applied from left to right, e.g. original :render
is first passed to first :wrap-render
function, result is then passed to second one and so on.
Sample mixin that forces re-render every second:
(def autorefresh-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)))
:transfer-state (fn [old-state state]
(merge state (select-keys old-state [::interval])))
:will-unmount (fn [state]
(js/clearInterval (::interval state)))})
(rum/defc timer < autorefresh-mixin []
[:div.timer (.toISOString (js/Date.))])
Imagine you have simple render function:
(defn render-label [text]
(sablono.core/html
[:div.label text]))
To convert it to React component, you create a mixin:
(def label-mixin {
{:render (fn [state]
[(render-label (:text state)) state])}})
Then you build React class from this single mixin:
(def label-class (rum/build-class label-mixin))
And define simple wrapper that creates React element from that class:
(defn label-ctor [text]
(rum/element label-class {:text text} nil))
Finally, you call ctor to get instance of element and mount it somewhere on a page:
(rum/mount (label-ctor "Hello") (.-body js/document))
This is a detailed breakdown of what happens inside of Rum. By using rum/defc
, everything can be simplified to a much more compact code:
(rum/defc label [text]
[:div.label text])
(rum/mount (label "Hello") (.-body js/document))
- Upgraded to React 0.13.3, Sablono 0.3.6, ClojueScript 1.7.48
- New API to access context:
child-context
,child-context-types
,context-types
(thx @Karanbir Toor, PR #37) - New
defcc
macro for when you only need React component, not the whole Rum state - [ BREAKING ] Component inner state (
:rum/state
) was moved fromprops
tostate
. It doesn’t change a thing if you were using Rum API only, but might break something if you were relaying on internal details - Deprecated
rum/with-props
macro, userum/with-key
orrum/with-ref
fns instead
- Allow components to refer to themselves (thx Kevin Lynagh, pull request #30)
- Support for multi-arity render fns (issue #23)
- Added
local
mixin
- Fixed argument destructuring in defc macro (issue #22)
will-update
anddid-update
lifecycle methods added (thx @Andrey Vasenin, pull request #18)
- Components defined via
defc/defcs
will havedisplayName
defined (thx @Ivan Dubrov, pull request #16) - Not referencing
requestAnimationFrame
when used in headless environment (thx @whodidthis, pull request #14)
- Compatibility with clojurescript 0.0-2758, macros included automatically when
(:require rum)
- Updated deps to clojurescript 0.0-2727, react 0.12.2-5 and sablono 0.3.1
- [ BREAKING ] New syntax for mixins:
(defc name < mixin1 mixin2 [args] body...)
- New
defcs
macro that adds additional first argument to render function:state
- Ability to specify
key
andref
to rum components viawith-props
- Fixed a bug when render-loop tried to
.forceUpdate
unmounted elements - Fixed a cursor leak bug in
reactive
mixin - Removed
:should-update
fromreactive
, it now will be re-rendered if re-created by top-level element - Combine
reactive
withstatic
to avoid re-rendering if component is being recreated with the same args
Rum was build on inspiration from Quiescent, Om and Reagent.
All heavy lifting done by React and ClojureScript.
Copyright © 2014–2015 Nikita Prokopov
Licensed under Eclipse Public License (see LICENSE).