• Stars
    star
    1,767
  • Rank 25,290 (Top 0.6 %)
  • Language
    HTML
  • License
    Eclipse Public Li...
  • Created over 9 years ago
  • Updated 5 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

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.10"]

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 render function (it will be passed as a first argument).
  4. You can then extract that atom from the component’s state and deref/swap!/reset! it as usual.
  5. Any change to the atom will force the component to update.

In practice, it’s quite convenient to use:

(rum/defcs stateful < (rum/local 0 ::key)
  [state label]
  (let [local-atom (::key state)]
    [:div { :on-click (fn [_] (swap! local-atom inc)) }
      label ": " @local-atom]))

(rum/mount (stateful "Click count") js/document.body)

Optimizing with shouldComponentUpdate

If your component accepts only immutable data structures as arguments, it may be a good idea to add the rum.core/static mixin:

(rum/defc label < rum/static [n text]
  [:.label (replicate n text)])

rum.core/static will check if the arguments of a component’s constructor have changed (using 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
(rum/mount (label 1 "xyz") body) ;; this won’t

Note that this is not enabled by default because a) comparisons can be expensive, and b) things will go wrong if you pass a mutable reference as an argument.

Writing your own mixin

Many applications have very specific requirements and custom optimization opportunities, so odds are you’ll be writing your own mixins.

Let’s see what a 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 the following state:

{ :rum/args ["Your name" ""]
  :rum/react-component <react-component> }

You can read the internal state by using the rum.core/defcs (short for “define component [and pass] state”) macro instead of rum.core/defc. It will pass state to the render function 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 a component’s lifecycle. Mixins are functions that are invoked at these stages to give you and opportunity to modify the state and/or do side effects to the world.

The following mixin will record the component’s mount time:

(rum/defcs time-label
  < { :will-mount (fn [state]
                    (assoc state ::time (js/Date.))) }
  [state label]
  [:div label ": " (str (::time state))])

As you can see, :will-mount is a function from state to state. It gives you a chance to populate, clean or modify state map the moment before the component has been mounted.

Another useful thing you can do in a mixin is to decide when to update a component. If you can get ahold of React component (notice that that’s different from Rum component, unfortunately; sorry), you can call rum.core/request-render to schedule this component’s update. To get React component, just look up :rum/react-component key in a state.

This mixin will update a 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)))
    :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)

Here’s a full list of callbacks you can define in a mixin:

{ :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
  :will-remount         ;; old-state, state ⇒ state
  :should-update        ;; old-state, state ⇒ boolean
  :will-update          ;; state            ⇒ state
  :did-update           ;; state            ⇒ state
  :will-unmount }       ;; state            ⇒ state

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])

One gotcha: don’t forget to return state from the mixin functions. If you’re using them for side-effects only, just return an unmodified state.

Working with atoms

Since Rum relies a lot at components being able to efficiently update themselves in reaction to events, it includes two facilities to build architectures around Atoms and watchers.

Cursors

If you have a complex state and need a component to interact with only a part of it, create a cursor using (rum.core/cursor-in ref path). Given atom with deep nested value and path inside it, cursor-in will create an atom-like structure that can be used separately from main atom, but will sync changes both ways:

(def state (atom { :color "#cc3333"
                   :user { :name "Ivan" } }))

(def user-name (rum/cursor-in 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.

Derived atoms

Use derived atoms to create “chains” and acyclic graphs of dependent atoms. derived-atom will:

  • Take N “source” refs
  • Set up a watch on each of them
  • Create “sink” atom
  • When any of source refs changes:
    • re-run function f, passing N dereferenced values of source refs
    • reset! result of f to the sink atom
  • return sink atom
  (def *a (atom 0))
  (def *b (atom 1))
  (def *x (derived-atom [*a *b] ::key
            (fn [a b]
              (str a \":\" b))))
  (type *x) ;; => clojure.lang.Atom
  @*x     ;; => 0:1
  (swap! *a inc)
  @*x     ;; => 1:1
  (reset! *b 7)
  @*x     ;; => 1:7

Derived atoms are like cursors, but can “depend on” multiple references and won’t sync changes back to the source if you try to update derived atom (don’t).

Interop with React

Native React component

You can access the raw React component by reading the state’s :rum/react-component attribute:

{ :did-mount (fn [state]
               (let [comp     (:rum/react-component state)
                     dom-node (js/ReactDOM.findDOMNode comp)]
                 (set! (.-width (.-style dom-node)) "100px"))
               state) }

React keys and refs

There’re three ways to specify React keys:

  1. If you need a key on Sablono tag, put it into attributes: [:div { :key "x" }]
  2. If you need a key on Rum component, use with-key:
(rum/defc my-component [str]
  ...)

(rum/with-key (my-component "args") "x")
  1. or, you can specify :key-fn in a mixin to calculate key based on args at component creation time:
(rum/defc my-component
  < { :key-fn (fn [x y z]
                (str x "-" y "-" z)) }
  [x y z]
  ...)

(my-component 1 2 3) ;; => key == "1-2-3"

:key-fn must accept same arguments your render function does.

Refs work the same way as options 1 and 2 for keys work:

  1. [:div { :ref "x" }]
  2. (rum/with-ref (my-component) "x")

Accessing DOM

[:div {:ref (fn [node] ...)}]

;; or

(let [ref (rum/create-ref)]
  [:input
    {:ref ref
     :on-change #(.log js/console (rum/deref ref))}])

⚠️ The helpers below are deprecated since usage of string refs has been deprecated in React itself. Instead use the API described above.

There’re couple of helpers that will, given state map, find stuff in it for you:

(rum/dom-node state)     ;; => top-level DOM node
(rum/ref      state "x") ;; => ref-ed React component
(rum/ref-node state "x") ;; => top-level DOM node of ref-ed React component

Custom class properties

To define arbitrary properties and methods on a component class, specify a :class-properties map in a mixin:

(rum/defc comp
  < { :class-properties { ... } }
  [:div]))

To define static properties on a component class, specify a :static-properties map in a mixin:

(rum/defc comp
  < { :static-properties { ... } }
  [:div]))

React context

New API
(rum/defcontext *context*)

(rum/defc context-consumer []
 (rum/with-context [value *context*]
   value)) ;; "hello"

(rum/defc context-provider []
  (rum/bind-context [*context* "hello"]
    (context-consumer))
Legacy API

⚠️ This API is deprecated in React and will be removed in future versions of Rum

To define child context

  1. Add dependency [cljsjs/prop-types "15.5.10-1"]
  2. (require [cljsjs.prop-types])
  3. Specify a :child-context function taking state and returning context map in a mixin:
(rum/defc theme
  < { :child-context
      (fn [state]
        (let [[color] (:rum/args state)]
          { :color color }))
      :static-properties
      { :childContextTypes {:color js/PropTypes.string} } }
  [color child]
  child)

React Hooks

There are Rum wrappers for the various React hooks. See doc strings for examples, and the React hooks reference for more details.

⚠️ Hooks can be used only in defc components with optional rum/static mixin. Using any other mixin or form of declaring a component will generate class-based React components that are not compatible with hooks. You should use either hooks or mixins in one component, two can't work together.

;; Takes initial value or value returning fn and returns a tuple of [value set-value!],
;; where `value` is current state value and `set-value!` is a function that schedules re-render.
(let [[x set-x!] (rum/use-state 0)]
  (set-x! (inc x)))

;; Takes reducing function and initial state value.
;; Returns a tuple of [value dispatch!], where `value` is current state value and `dispatch` is a function that schedules re-render.
(rum/use-reducer reducer-fn initial-value)

;; Takes setup-fn that executes either on the first render or after every update.
;; The function may return cleanup-fn to cleanup the effect, either before unmount or before every next update.
;; Calling behavior is controlled by deps argument.
(rum/use-effect!
  (fn []
    (.addEventListener js/document "keydown" js/console.log)
    #(.removeEventListener js/document "keydown" js/console.log))
  [])

;; Takes callback function and returns memoized variant, memoization is done based on provided deps collection.
(rum/defc component [x]
  (let [on-change (rum/use-callback #(js/console.log % x) [x])]
    [input-field {:on-change on-change}]))

;; Takes a function, memoizes it based on provided deps collection and executes immediately returning a result.
(let [x (rum/use-memo #(expensive-computation v) [v])])

;; Takes a value and puts it into a mutable container which is persisted for the full lifetime of the component.
(rum/defc component []
  (let [ref (rum/use-ref nil)]
    (rum/use-effect!
      #(.log js/console (rum/deref ref)))
    [:input {:ref ref}]))

React Fragment

Using :<> as the tag in a markup vector creates a React Fragment, allowing you to render multiple components without a wrapping element.

[:<>
  [:span]
  [:div]
  [:span]]

;; <span></span><div></div><span></span>

Server-side rendering

When used from cljs Rum delegates serialization to ReactDOM library. If used from clj/cljc, Rum works as a traditional template engine à la Hiccup:

  1. Rum’s project.clj dependency becomes [rum "0.12.10" :exclusions [cljsjs/react cljsjs/react-dom]
  2. Import rum.core as usual.
  3. Define components using rum/defc or other macros as usual.
  4. Instead of mounting, call rum/render-html to render into a string.
  5. Generate the HTML page using that string.
  6. On the client side, mount (but using rum/hydrate) 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=\"\">hello</div>"

;; on a client
(rum/hydrate (my-comp "hello") js/document.body)

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 and :will-mount from mixins would be called at the component’s construction time.

Support

Talks

App templates

Libraries

  • Reforms, Bootstrap 3 forms
  • rum-mdl, Material design lite components
  • derivatives, creates chains of derived values from an atom
  • citrus, state coordination framework (previously known as scrum)
  • Antizer Ant Design component library
  • data-frisk-rum, display current value of data

Examples

Acknowledgements

Rum was build on inspiration from Quiescent, Om and Reagent.

All heavy lifting done by React and ClojureScript.

License

Copyright © 2014 Nikita Prokopov, 2020 Roman Liutikov

Licensed under Eclipse Public License (see LICENSE).

More Repositories

1

FiraCode

Free monospaced font with programming ligatures
Clojure
74,361
star
2

AnyBar

OS X menubar status indicator
Objective-C
5,856
star
3

datascript

Immutable database and Datalog query engine for Clojure, ClojureScript and JS
Clojure
5,353
star
4

vscode-theme-alabaster

A light theme for Visual Studio Code
Clojure
386
star
5

Clojure-Sublimed

Clojure support for Sublime Text 4
Clojure
349
star
6

tongue

Do-it-yourself i18n library for Clojure/Script
Clojure
305
star
7

uberdeps

Uberjar builder for deps.edn
Clojure
299
star
8

Universal-Layout

Пакет из английской и русской раскладок, спроектированных для удобного совместного использования
Shell
291
star
9

font-writer

Monospaced font for long-form writing
242
star
10

sublime-scheme-alabaster

Minimalist color scheme for Sublime Text 3
234
star
11

datascript-chat

Sample SPA using DataScript and core.async
Clojure
160
star
12

grumpy

Minimalistic blog engine
Clojure
141
star
13

compact-uuids

Compact 26-char URL-safe representation of UUIDs
Clojure
126
star
14

net.async

Network commucations with clojure.core.async interface
Clojure
123
star
15

sublime-scheme-writer

A color scheme for focused long-form writing
119
star
16

clojure-future-spec

A backport of clojure.spec for Clojure 1.8
Clojure
115
star
17

intellij-alabaster

Alabaster color scheme for IntelliJ IDEA
102
star
18

datascript-transit

Transit handlers for DataScript database and datoms
Clojure
100
star
19

sublime-profiles

Profile Switcher for Sublime Text
Python
81
star
20

datascript-todo

DataScript ToDo Sample Application
Clojure
78
star
21

persistent-sorted-set

Fast B-tree based persistent sorted set for Clojure/Script
Clojure
78
star
22

tonsky.github.io

HTML
65
star
23

clojure-warrior

Visual Studio Code extension for Clojure development
TypeScript
57
star
24

cljs-drag-n-drop

Sane wrapper around Drag-n-Drop DOM API
Clojure
55
star
25

vec

React.js + Immutable.js vector editor
JavaScript
51
star
26

clojure.unicode

Unicode symbols for Clojure
Clojure
48
star
27

clj-simple-router

Simple order-independent Ring router
Clojure
48
star
28

41-socks

Simple match game in cljs+om+react
Clojure
37
star
29

remote-require

Require any Clojure snippet from anywhere in the Internet
Clojure
33
star
30

Sublime-Executor

Run any executable from your working dir in Sublime Text
Python
32
star
31

cljs-skeleton

Skeleton CLJS client/server app with WS, Transit, Rum
Clojure
30
star
32

Heroes

A turn-based tactical game in ClojureScript, DataScript and Rum
Clojure
30
star
33

icfpc2019-rust

Re-implementaion of https://github.com/tonsky/icfpc2019 in Rust to compare performance
Rust
28
star
34

alabaster-lighttable-skin

Light skin & theme for LightTable
CSS
27
star
35

clj-reload

Clojure
27
star
36

openshift-clojure

Clojure/lein openshift cartridge template
Shell
26
star
37

datascript-storage-sql

SQL Storage implementation for DataScript
Clojure
23
star
38

sublime-scheme-commander

Retro color scheme for Sublime Text
23
star
39

sublime-clojure-repl

Basic Clojure REPL for Sublime Text
Python
22
star
40

Levinson-Layout

Keymap & EN/RU layouts for Levinson 40% split ortholinear keyboard
C
21
star
41

boot-anybar

A boot task reporting build status to AnyBar
Clojure
18
star
42

extend-clj

Easily extend clojure.core built-in protocols
Clojure
17
star
43

down-the-rabbit-hole

Entry to Ludum Dare 48
Clojure
17
star
44

bloknote

Fast online notepad
Clojure
16
star
45

sublime-color-schemes

Fun and simple color schemes for Sublime Text
Rust
16
star
46

katybot

Campfire bot written in Clojure
Clojure
15
star
47

toml-clj

Fast TOML parser for Clojure
Java
14
star
48

java-graphics-benchmark

Java Graphics benchmark
Java
13
star
49

Helix-Layout

C
13
star
50

sane-math

Clojure/Script library for infix (normal) math expressions
Clojure
12
star
51

datascript-menu

JavaScript
11
star
52

DarkModeToggle

Statusbar app to quickly toggle between light and dark modes
Swift
11
star
53

icfpc2021

Clojure
11
star
54

humble-ants

Clojure
10
star
55

advent-of-code

https://adventofcode.com/
Clojure
9
star
56

icfpc2019

Clojure
7
star
57

tonsky.me

Clojure
7
star
58

tgadmin

Clojure
7
star
59

jwm

Objective-C++
6
star
60

homm

Clojure
5
star
61

GMTKJam2022

GDScript
5
star
62

advent2018

Solutions to https://adventofcode.com/2018 in Clojure
Clojure
5
star
63

spectre

Fantom
3
star
64

imdbparse

Parser for IMDb text database
Clojure
3
star
65

icfpc2022

Clojure
3
star
66

clojure-bits

Clojure
3
star
67

tonsky

2
star
68

2017-10-Reactive

JavaScript
2
star
69

2017-05-RigaDevDays

JavaScript
2
star
70

clojure-bits-server

Clojure
2
star
71

lein-figwheel-immutant

[tonsky/figwheel-sidecar-immutant "0.5.9"]
Clojure
2
star
72

2018-05-UWDC

http://tonsky.me/2018-05-UWDC/slides/
JavaScript
2
star
73

codingame-fall-2022

Coding Games Fall Challenge 2022
Clojure
2
star
74

datascript-perf

Datasets for DataScript perf testing
Clojure
2
star
75

grumpy_video

1
star
76

datascript_compiler_race

Clojure
1
star
77

roam-calculator

Shell
1
star
78

icfpc2023

Clojure
1
star
79

ldjam53

GDScript
1
star
80

glutin_resize_issue

Rust
1
star