SkillAgentSearch skills...

Rum

Simple, decomplected, isomorphic HTML UI library for Clojure and ClojureScript

Install / Use

/learn @tonsky/Rum
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<p align="center"><img src="https://s.tonsky.me/imgs/rum_logo.svg" style="height: 400px;"></p>

[!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

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?

Using Rum

Add to project.clj: [rum "0.12.11"]

API Docs & Articles

cljdoc badge

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-screen for allowFullScreen)
  • You can include :id and :class there as well
  • :class can 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:

  1. We’re adding the rum.core/reactive mixin to the component.
  2. We’re using rum.core/react instead of deref in 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:

  1. Each component in Rum has internal state associated with it, normally used by mixins and Rum internals.
  2. rum.core/local creates a mixin that will put an atom into the component’s state.
  3. rum.core/defcs is used instead of rum.core/defc. It allows you to get hold of the components’s state in the rend
View on GitHub
GitHub Stars1.8k
CategoryDevelopment
Updated1d ago
Forks127

Languages

HTML

Security Score

95/100

Audited on Mar 28, 2026

No findings