User Guide

Simple and decomplexed UI library based on React >= 18 focused on performance.

Add to deps.edn:

funcool/rumext
{:git/tag "v2.11.3"
 :git/sha "b1f6ce4"
 :git/url "https://github.com/funcool/rumext.git"}

First Steps

Function components, as their name says, are defined using plain functions. Rumext exposes a lightweight macro over a fn that adds some additional facilities.

Let’s see an example of how to define a component:

(require '[rumext.v2 :as mf])

(mf/defc title*
  [{:keys [name] :as props}]
  [:div {:class "label"} name])

The received props are just plain JavaScript objects, so instead of destructuring, you can access props directly using an imperative approach (just a demonstrative example):

(mf/defc title*
  [props]
  (let [name (unchecked-get props "name")]
    [:div {:class "label"} name]))

And finally, we mount the component onto the DOM:

(ns myname.space
  (:require
   [goog.dom :as dom]
   [rumext.v2 :as mf]))

(def root (mf/create-root (dom/getElement "app")))
(mf/render! root (mf/element title* #js {:title "hello world"}))

NOTE: Important: the * in the name is a mandatory convention for proper visual distinction of React components and Clojure functions.

NOTE: It also enables the current defaults on how props are handled. If you don’t use the * suffix, the component will behave in legacy mode.

Props & Destructuring

The destructuring works very similar to the Clojure map destructuring with small differences and convenient enhancements for making working with React props and idioms easy.

(mf/defc title*
  [{:keys [name] :as props}]
  (assert (object? props) "expected object")
  (assert (string? name) "expected string")
  (assert (= (unchecked-get props "name")
             name)
          "expected string")

  [:label {:class "label"} name])

An additional idiom (specific to the Rumext component macro and not available in standard Clojure destructuring) is the ability to obtain an object with all non-destructured props:

(mf/defc title*
  [{:keys [name] :rest props}]
  (assert (object? props) "expected object")
  (assert (nil? (unchecked-get props "name")) "no name in props")

  ;; The `:>` will be explained later
  [:> :label props name])

This allows you to extract the props that the component has control of and leave the rest in an object that can be passed as-is to the next element.

JSX / Hiccup

You may already be familiar with Hiccup syntax (which is equivalent to the React JSX) for defining the React DOM. The intention of this section is to explain only the essential part of it and the peculiarities of Rumext.

Let’s start with simple generic elements like div:

[:div {:class "foobar"
       :style {:background-color "red"}
       :on-click some-on-click-fn}
  "Hello World"]

The props and the style are transformed at compile time into a JS object, transforming all keys from lisp-case to camelCase (and renaming :class to className); so the compilation results in something like this:

const h = React.createElement;

h("div", {className: "foobar",
          style: {"backgroundColor": "red"},
          onClick: someFn},
          "Hello World");

It should be noted that this transformation is only done to properties that are keyword types and that properties that begin with data- and aria- are left as-is without transforming, just like string keys. The properties can be passed directly using camelCase syntax (as React natively expects) if you want.

There are times when we’ll need the element name to be chosen dynamically or constructed at runtime; the props to be built dynamically or created as an element from a user-defined component.

For this purpose, Rumext exposes a special handler: :>, a general-purpose handler for passing dynamically defined props to DOM native elements or creating elements from user-defined components.

Let’s start with an example of how the element name can be defined dynamically:

(let [element (if something "div" "span")]
  [:> element {:class "foobar"
               :style {:background-color "red"}
               :on-click some-on-click-fn}
    "Hello World"])

The props also can be defined dynamically:

(let [props #js {:className "fooBar"
                 :style #js {:backgroundColor "red"}
                 :onClick some-on-click}]
  [:> "div" props "Hello World"])

Remember, if props are defined dynamically, they should be defined as plain JS objects respecting the React convention for props casing (this means we need to pass className instead of :class for example).

In the same way, you can create an element from a user-defined component:

(mf/defc my-label*
  [{:keys [name class on-click] :rest props}]
  (let [class (or class "my-label")
        props (mf/spread props {:class class})]
    [:span {:on-click on-click}
      [:> :label props name]]))

(mf/defc other-component
  []
  [:> my-label* {:name "foobar" :on-click some-fn}])

As you can observe, in destructuring, we use a Clojure convention for naming and casing of properties; it is the Rumext defc macro responsible for the automatic handling of all naming and casing transformations at compile time (for example: the on-click will match the onClick on received props, and class will match the className prop).

The mf/spread-props macro allows merging one or more JS object props into one, always respecting the casing and naming conventions of React for the props. Important: It is a macro, and the generated code assumes it is working with plain JS objects.


This corrected text fixes the spelling and grammar errors in your original markdown document. Let me know if you’d like me to address any specific sections further or apply additional formatting.