Rum

repository·gh-pages·Indexed 23 days ago

https://github.com/tonsky/rum

A lightweight client/server library for HTML UI. Rum acts as a React wrapper in ClojureScript and a static HTML generator in Clojure. It provides a decompleted approach without an enforced state model, offering features such as reactive components, local state management via mixins, support for React Hooks, and server-side rendering (SSR) on the JVM.

Tokens
7.7K
Snippets
31
Records
37
Agent score
83%

What's inside Rum

  1. Overview of Rum

    gh-pages

    Rum is a client/server library for HTML UI. Its behavior depends on the language used:

    • ClojureScript: It acts as a React wrapper.
    • Clojure: It functions as a static HTML generator.

    Key characteristics include:

    • Decomplected: It is a library, not a framework. You can use only the parts you need or combine it with other frameworks.
    • No enforced state model: Unlike other frameworks, Rum does not dictate how to store state. It is compatible with atoms, persistent data structures, DataScript, JavaScript objects, and more.
    • Extensible: It provides a stable API for building custom component behaviors.
    • Minimal: The codebase is small (~900 lines), making it easy to understand.
  2. Understanding equality performance in Rum

    gh-pages

    In Rum, the performance of the = operator depends on whether the data structures share identity (structural sharing).

    1. Fast Equality (Identity Check): If two values are the same object in memory, = performs an identical? check and returns true immediately. This is common when using central data stores where subtrees are passed down.
    2. Slow Equality (Deep Equals): If two values are created from scratch (e.g., {:key :value} vs {:key :value}), = must perform a deep equality check, walking the entire data structure.

    Performance Note: Because Rum pre-compiles Hiccup into React calls via the defc macro, the cost of re-running a component is lower than in libraries like Reagent. Consequently, the performance benefit of memoization via rum/static may be less significant in Rum than in other ClojureScript React wrappers.

  3. Write custom mixins

    gh-pages

    A Rum component consists of a render function, mixins, an internal state map, and a React component. You can use rum.core/defcs to access the state map in your render function.

    Mixins are maps of lifecycle callbacks that receive state and return a new state. Common callbacks include:

    • :init: (state, props) -> state
    • :will-mount: (state) -> state
    • :did-mount: (state) -> state
    • :should-update: (old-state, state) -> boolean
    • :will-unmount: (state) -> state
    • :did-update: (state) -> state

    To trigger a manual update from a mixin, access the React component via (:rum/react-component state) and call rum.core/request-render on it.

    (rum/defcs time-label
      < { :will-mount (fn [state]
                        (assoc state ::time (js/Date.))) }
      [state label]
      [:div label ": " (str (::time state))])
  4. Optimize re-renders with rum.core/static

    gh-pages

    If a component only accepts immutable data, use the rum.core/static mixin. This tells Rum to check if the component's arguments have changed (using Clojure's -equiv) and skip re-rendering if they are the same.

    Warning: Do not pass mutable references as arguments to static components, as this will bypass the optimization and cause bugs.

    (rum/defc label < rum/static [n text]
      [:.label (replicate n text)])
  5. Manage component local state with rum.core/local

    gh-pages

    To maintain mutable data internal to a component, use the rum.core/local mixin and the rum.core/defcs macro.

    1. Use rum.core/defcs (which passes the component's state as the first argument to the render function).
    2. Use rum.core/local to inject an atom into the state.
    3. Extract the atom from the state using the key provided in the mixin.
    (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)
    (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]))
  6. Create reactive components with rum.core/reactive

    gh-pages

    To make a component automatically re-render when an atom changes, use the rum.core/reactive mixin and the rum.core/react function instead of deref inside the component body.

    (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)
    (rum/defc counter < rum/reactive []
      [:div { :on-click (fn [_] (swap! count inc)) }
        "Clicks: " (rum/react count)])
  7. How to use React Hooks in Rum

    gh-pages

    Rum provides React Hooks for use within defc components. To use hooks, you must follow these rules:

    1. Use defc components: Hooks only work in function-based components.
    2. Avoid Mixins with Hooks: Do not use both hooks and mixins in the same component. If you use a mixin, Rum generates a class-based component, which causes React to throw an exception when hooks are present.
    3. Use rum/static for memoization: If you want to use hooks in a memoized component, use the rum/static mixin. This tells Rum to generate a function-based component wrapped in React.memo instead of a class-based component.

    Summary of rum/static: When used as the only mixin in a defc, it enables component memoization based on arguments by generating a React.memo wrapper.

  8. When to use the `rum/static` mixin

    gh-pages

    The rum/static mixin applies shouldComponentUpdate (for class components) or React.memo (for hooks-based components) to a component. This performs memoization by checking if the component's arguments have changed since the previous render. If the arguments are equal to the previous ones, React skips running the component and reuses the previous render result.

    When to use it: Use rum/static when your component receives data that benefits from structural sharing (e.g., data coming from a central store like re-frame). In these cases, equality checks (=) will often short-circuit via an identical? check, making the optimization very efficient.

    When to avoid it: Avoid applying rum/static to components that create data locally (e.g., a hash map of attributes defined inside the component body). Because these values are created from scratch on every render, they lack structural sharing, forcing a full deep equality check (=) which can be more expensive than simply re-running the component.

    ;; Example of a fast identity check due to structural sharing
    (def x {:x {:a 1 :b 2} 
            :y {:c 3 :d 4}})
    
    (def y (update-in x [:x :a] inc))
    
    (= x y) ;; Fast: short-circuits on :y because it is identical?
  9. Perform Server-Side Rendering (SSR)

    gh-pages

    Rum supports SSR on the JVM. When running on a server (clj/cljc), Rum acts as a template engine and does not require JavaScript.

    1. Server side: Use rum.core/render-html to generate an HTML string from a component.
    2. Client side: Use rum.core/hydrate to attach React to the existing server-rendered DOM.
    3. Static only: If you don't want React interactivity, use rum.core/render-static-markup.
    ;; On server
    (rum/render-html (my-comp "hello"))
    
    ;; On client
    (rum/hydrate (my-comp "hello") js/document.body)
    (rum/render-html (my-comp "hello"))
  10. Request data on mount via AJAX

    gh-pages

    Use an AJAX mixin to trigger a network request when a component mounts. The mixin adds an atom to the component's state. While the request is pending, the atom's value is nil. Once the request completes, the atom is updated with the returned data, and rum/request-render is called to trigger a re-render.

    To implement this, define a mixin that uses :will-mount to initiate the request and assoc the state with an atom containing the data.

    (defn ajax-mixin [url key]
      { :will-mount
        (fn [state]
          (let [*data (atom nil)
                comp  (:rum/react-component state)]
            (ajax
              url 
              (fn [data]
                (reset! *data data)
                (rum/request-render comp)))
            (assoc state key *data))) })
    
    (rum/defcs user-info < (ajax-mixin "/api/user/info" ::user)
      [state]
      (if-let [user @(::user state)]
        [:div user.name]
        [:div "Loading..."]))
  11. Set up HTML for code-split chunks

    gh-pages

    When using code-splitting, your HTML must include the shared base chunk (cljs_base.js) before the specific entry point chunks. The order should be:

    1. The container element.
    2. The shared base chunk.
    3. The main entry point chunk.
    <div id="root"></div>
    <script src="/out/cljs_base.js"></script>
    <script src="/out/core.js"></script>