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 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 you 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.gi
Extensible API is stable and explicitly defined, including API between Rum internals. It lets you build custom behaviours that change components in a significan ways.
Minimal codebase You can become Rum expert just by reading its source code (~700 lines)
Rum:
- does not dictate how to store your state,
- has server-side rendering,
- is much smaller.
- Cognician, coaching platform
- Attendify, mobile app builder
- PartsBox.io, inventory management
- modnaKasta, online shopping
- TourneyBot, frisbee tournament app
Add to project.clj: [rum "0.8.3"]
Use rum.core/defc (short of “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 tag, its id and classes:
:span
:span#id
:span.class
:span#id.class
:span.class.class2By default, if you omit 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 :classmight 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 an 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 tagsChildren might 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 unescaped string into a tag, add :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 something like that:
<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>mount is usually used just once in an app lifecycle to mount 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.
Simplest way to update your app is to mount it again:
(rum/mount (repeat-label 5 "abc") js/document.body)
(js/setTimeout
#(rum/mount (repeat-label 3 "xyz") js/document.body)
1000)Better way is to use (rum.core/request-render react-component) which will schedule update until next animation frame. It will also throttle duplicate update calls if you happen to call request-render more than once:
(rum/defc my-app []
[:div (rand)])
(let [react-comp (rum/mount (my-app) js/document.body)]
(js/setTimeout #(rum/request-render react-comp) 1000))Note that request-render accepts React component, not Rum component. One way to get it is to save the return value of mount, we’ll see other ways in a “Writing your own mixin” section.
Also note that request-render does not let you change component arguments. It expects that component is responsible to get its state in a render functoin itself.
This is already enough to build a simple click counter:
(def count (atom 0))
(rum/defc counter []
[:div { :on-click (fn [_] (swap! count inc)) }
"Clicks: " @count])
(let [react-comp (rum/mount (counter) js/document.body)]
(add-watch count ::render
(fn [_ _ _ _]
(rum/request-render react-comp))))Rum offers mixins as a way to hook into component lifecycle and extend its capabilities or change its behaviour.
One very common use-case is for component to update when some reference changes. Rum has 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)There’re two things happening:
- We’ve added
rum.core/reactivemixin to the component. - We’ve used
rum.core/reactinstead ofderefin a component body.
This will set up a watch on count atom and will automatically call rum.core/request-render on the component each time that reference has changed.
If you have a complex state and need component to interact with only some part of it, create a derived cursor using (rum.core/cursor ref path):
(def state (atom { :color "#cc3333"
:user { :name "Ivan" } }))
(def user-name (rum/cursor 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.
Sometimes you need to keep track of some mutable data just inside component and nowhere else. Rum provides rum.core/local mixin. It’s a little trickier to use, so hold on:
- Each compoent 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 component’s state.- You use
rum.core/defcsinstead ofrum.core/defcto get hold of components’s state in a render function. - You extract that atom from state and
deref/swap!/reset!it as usual. - Any change in that atom will force component to update.
In practice, it’s quite convenient to use:
(rum/defcs stateful < (rum/local 0) [state label]
(let [local-atom (:rum/local state)]
[:div { :on-click (fn [_] (swap! local-atom inc)) }
label ": " @local-atom]))
(rum/mount (stateful "Click count") js/document.body)You can change :rum/local key to any other by specifying second argument to rum/local, e.g.
(rum/defcs input < (rum/local "" ::text)
[state]
(let [text-atom (::text state)]
[:input { :type "text"
:value @text-atom
:on-change (fn [e]
(reset! text-atom (.. e -target -value))) }]))If you component accepts only immutable data structures as arguments, it might be a good idea to add rum.core/static mixin:
(rum/defc label < rum/static [n text]
[:.label (repeat n text)])rum.core/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/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
Note that this is not enabled by default because a) comparisons might be expensive, and b) it will work wrong if you pass mutable reference as an argument.
Many applications have very specific requirements and custom optimization opportunities, so odds are you’ll be also writing your own mixins.
Let’s see what Rum component really is. Each Rum component has:
- A render function
- One or more mixins
- An internal state map
- A corresponding React component
For example, if we have this component defined:
(rum/defc input [label value]
[:label label ": "
[:input { :value value }]])
(input "Your name" "")It will have following state:
{ :rum/id <int>
:rum/args ["Your name" ""]
:rum/react-component <react-component> }You can read internal state by using rum.core/defcs (short of “define component [and pass] state”) macro instead of rum.core/defc. It will pass state 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 component lifecycle. Mixins are functions that are invoked at these stages and modify state and/or do side effects to the world.
This mixin will record component’s mount time:
(rum/defcs time-label < { :did-mount (fn [state]
(assoc state ::time (Date.))) }
[state label]
[:div label ": " (::time state)])As you can see, :did-mount is a function from state to state and can populate, clean or modify it just after component has been mounted.
This mixin will update 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)))
:transfer-state (fn [old-state state]
(assoc state ::interval (::interval old-state)))
: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)Two gotchas:
- Don’t forget to return
statefrom mixin functions. If you’re using them for side-effects only, just return unmodifiedstate. - If you put something into state in
:did-mount/:will-mount, write a:transfer-statefunction that will move that attribute from old component instance to the new one.
Here’s a full list of callbacks you can define in a mixin:
{ :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 }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])You can access raw React component by reading :rum/react-component attribute from state:
{ :did-mount (fn [state]
(let [comp (:rum/react-component state)
dom-node (js/ReactDOM.findDOMNode comp)]
(set! (.-width (.-style dom-node)) "100px"))
state) }You can’t specify React key from inside component, but you can do so when you create it:
(rum/defc my-component [str]
...)
(rum/with-key (my-component "args") 77)To define arbitrary properties and methods on a component class, specify :class-properties map in a mixin:
(rum/defc comp < { :class-properties { ... } }
[:div]))If used from clj/cljc, Rum works as a traditional template engine à la Hiccup:
- Import
rum.coreas usual. - Define components using
rum/defcor other macros as usual. - Instead of mounting, call
rum/render-htmlto render into a string. - Generate HTML page using that string.
- On a client, mount the same component over the node where you rendered your server-side component.
(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.querySelector "[data-reactroot]))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, :will-mount and :did-mount from mixins would be called at the component construction time.
Ask for help on Gitter channel
- In this repo see examples/rum/examples/. Live version
- DataScript Chat app
- DataScript ToDo app
- DataScript Menu app
- 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
- Hangout about Rum (in Russian)
rum/render-static-markupcall 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)
- Add
rum.core/unmountfunction (thx emnh, issue #61)
- Retain
:arglistsmetadata on vars defined byrum/def*macros (thx aJchemist, PR #60)
- Migrated to React 15.0.1
- Optimized server-side rendering (~4× faster than Rum 0.7.0, ~2-3× faster than Hiccup 1.0.5)
- Server-side rendering via
rum/render-html(thx Alexander Solovyov)
- [ BREAKING ] Updated to React 0.14.3 (thx Andrey Antukh, PR #53)
- Added
:class-propertiesto define arbitrary properties on a React class (thx Karanbir Toor, PR #44) - [ BREAKING ] Removed support for
:child-context-typesand:context-types. Use{ :class-properties { :childContextTypes ..., :contextTypes ... } }instead.
- Check for
setTimeoutin global scope instead of in window (thx Alexander Solovyov, PR #43)
- Fixed bug with rum macros emitting wrong namespace. You can now require
rum.coreunder any alias you want (thx Stuart Hinson, PR #42)
- [ BREAKING ] Core namespace was renamed from
rumtorum.coreto supress CLJS warnings
- 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
defccmacro for when you only need React component, not the whole Rum state - [ BREAKING ] Component inner state (
:rum/state) was moved frompropstostate. 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-propsmacro, userum/with-keyorrum/with-reffns instead
- Allow components to refer to themselves (thx Kevin Lynagh, pull request #30)
- Support for multi-arity render fns (issue #23)
- Added
localmixin
- Fixed argument destructuring in defc macro (issue #22)
will-updateanddid-updatelifecycle methods added (thx Andrey Vasenin, pull request #18)
- Components defined via
defc/defcswill havedisplayNamedefined (thx Ivan Dubrov, pull request #16) - Not referencing
requestAnimationFramewhen 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
defcsmacro that adds additional first argument to render function:state - Ability to specify
keyandrefto rum components viawith-props
- Fixed a bug when render-loop tried to
.forceUpdateunmounted elements - Fixed a cursor leak bug in
reactivemixin - Removed
:should-updatefromreactive, it now will be re-rendered if re-created by top-level element - Combine
reactivewithstaticto 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, Ŝablono and ClojureScript.
Copyright © 2014–2016 Nikita Prokopov
Licensed under Eclipse Public License (see LICENSE).