Rum
Simple, decomplected, isomorphic HTML UI library for Clojure and ClojureScript
Install / Use
/learn @tonsky/RumREADME
[!CAUTION] Project is in support mode. No new development is happening. If you want to become a maintainer, let me know.
Rum is a client/server library for HTML UI. In ClojureScript, it works as React wrapper, in Clojure, it is a static HTML generator.
Table of Contents
- Principles
- Comparison to other frameworks
- Who’s using Rum?
- Using Rum
- Support
- Acknowledgements
- License
Principles
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).
Comparison to other frameworks
Rum:
- does not dictate how to store your state,
- has server-side rendering,
- is much smaller.
Who’s using Rum?
- Arc Studio, collaborative screenwriting app
- Cognician, coaching platform
- Attendify, mobile app builder
- PartsBox.io, inventory management
- kasta.ua, online marketplace
- ChildrensHeartSurgery.info, heart surgery statistics
- Mighty Hype, cinema platform (server-side rendering)
- БезопасныеДороги.рф, road data aggregator
- TourneyBot, frisbee tournament app
- PurposeFly, HR 2.0 platform
- Simply, Simple direct life insurance
- Oscaro.com, online autoparts retailer
- Lupapiste, building permit issuance and management
- Newsroom AI, personalised content delivery platform
- Lambdahackers, reddit-like groups for programmers
- Breast Predict, predicting survival after adjuvant treatment for breast cancer
- Prostate Predict, prognostic model for men newly diagnosed with non-metastatic prostate cancer
- Wobaka, CRM system
- Gatheround, online events
- Carrot / OpenCompany, company updates
- UXBOX, the open-source solution for design and prototyping
- Takeoff, automated grocery fulfillment solution
Using Rum
Add to project.clj: [rum "0.12.11"]
API Docs & Articles
Defining a component
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:
- Use kebab-case keywords for attributes (e.g.
:allow-full-screenforallowFullScreen) - You can include
:idand:classthere as well :classcan be a string or a sequence of strings:style, if needed, must be a map with kebab-case keywords- event handlers should be arity-one functions
[: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>"}}]
Rendering component
Given this code:
(require [rum.core :as rum])
(rum/defc repeat-label [n text]
[:div (replicate 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.
Performance
Daiquiri, Rum's Hiccup compiler, pre-compiles certain Clojure forms that return Hiccup (for a list of these forms see compile-form implementations) into React calls. When the compiler is not able to pre-compile a form it defers this operation to the runtime. Runtime interpretation is slower, the suggestion is to use Clojure forms that are handled by compile-form, when it makes sense.
(rum/defc component []
[:ul
(for [n (range 10)]
[:li n]) ;; `for` is a known form with a well defined syntax, thus Hiccup is pre-compiled
(map (fn [n]
[:li n]) ;; `map` is a generic higher-order function, can't reliably pre-compile, falling back to interpretation
(range 10))])
To be informed about such code there's compiler flag that enables build warnings
(rum.core/set-warn-on-interpretation! true)
Updating components manually
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)
Reactive components
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:
- We’re adding the
rum.core/reactivemixin to the component. - We’re using
rum.core/reactinstead ofderefin 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.
Component’s local state
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:
- Each component in Rum has internal state associated with it, normally used by mixins and Rum internals.
rum.core/localcreates a mixin that will put an atom into the component’s state.rum.core/defcsis used instead ofrum.core/defc. It allows you to get hold of the components’s state in the rend
