Liking cljdoc? Tell your friends :D

rumext

Simple and Decomplected UI library based on React (originated as fork from rum).

Using rumext

Add to deps.edn:

funcool/rumext {:mvn/version "2020.03.23-1"}

Differences with rum

This is the list of the main differences that rumext introduces vs rum:

  • clear distintion between class components and function components.
  • function components uses React Hooks behind the scenes for provide local state, pure components and reactivity (the ability of rerender component on atom change).
  • a clojurescript friendly abstractions for React Hooks.
  • more idiomatic macro for define class based components, that allows include lifecycle methods directly (without need to create an ad-hoc mixin).
  • the component body is compiled statically (never interprets at runtime thanks to hicada).
  • the react class is build lazily (no react class is build until first call to the component). This allows reduce load time when you have a lot of components but on the "current page" only a few of them are used.
  • don't use varargs on components (better performance)
  • uses new react lifecyle methods (all deprecated lifecyles are removed).
  • does not includes the cursors and derived-atoms; that stuff is delegated to third party libraries like lentes.
  • no server-side rendering

WARNING: this is not intended for general use, it is mainly implemented to be used in uxbox and released as separated project for conveniendce. Don't expect compromise for backward compatibility.

Class Components

Defining a Class Component

Let's see an example of how to use rumext macros for define class based components.

(require '[rumext.alpha as mf])

(mf/def local-state
  :desc "A component docstring/description (optional)."
  :mixins [(mf/local 0)]
  :init
  (fn [own props]
    (println "Component initialized")
    own)

  :render
  (fn [own {:keys [title] :as props}]
    (let [*count (::mf/local state)]
      [:div {:on-click #(swap! *count inc)}
        [:span title ": " @*count]])))

(mf/def parent-component
  :render
  (fn [own props]
    [:section
     [:h1 "Some title"]
     [:& local-state {:title "Some title"}]]))

This example uses the mf/local mixin that provides a local mutable stat to the component.

Reactive Component

You need to use the mf/reactive mixin and mf/react (instead of deref) for deref the atom. Let's see an example:

(def count (atom 0))

(mf/def counter
  :mixins [mf/reactive]
  :render
  (fn [own props]
    [:div {:on-click #(swap! count inc)}
      [:span "Clicks: " (mf/react count)]]))

(mf/mount (mf/element counter) js/document.body)

Pure Component

If you have a component that only accepts immutable data structures, you can use the mf/memo mixin for avoid unnecesary renders if arguments does not change between them.

(mf/def title
  :mixins [mf/memo]
  :render
  (fn [_ {:keys [name]}]
    [:div {:class "label"} name]))

So if we manuall trigger the component mounting, we will obtain:

(mf/mount (mf/element title {:name "ciri"}) body)   ;; first render
(mf/mount (mf/element title {:name "ciri"}) body)   ;; second render: don't be rendered
(mf/mount (mf/element title {:name "geralt"}) body) ;; third render: re-render
(mf/mount (mf/element title {:name "geralt"}) body) ;; forth render:  don't be rendered

The mf/memo mixin uses indentical? for compare props. If you want equality by value (using the = function), you can use mf/pure mixin.

There also mf/static mixin that completelly prevents rerendering.

Lifecycle Methods

Here’s a full list of lifecycle methods supported by rumext:

:init           ;; state, props     => state (called once, on component constructor)
:did-catch      ;; state, err, inf  => state (like try/catch for components)
:did-mount      ;; state            => state
:did-update     ;; state, snapshot  => state
:after-render   ;; state            => state (did-mount and did-update alias)
:should-update  ;; old-state, state => bool  (the shouldComponentUpdate)
:will-unmount   ;; state            => state
:derive-state   ;; state            => state (similar to legacy will-update and will-mount)
:make-snapshot  ;; state            => snapshot

A mixin is a map with a combination of this methods. And a component can have as many mixins as you need.

If you don't understand some methods, refer to react documentation: https://reactjs.org/docs/react-component.html

Function Components

Function components as it's name says, are defined using plain functions. Rumext exposes a lightweigh macro over a fn that convert props from js-object to cljs map (shallow) and exposes a facility for docorate (wrap) with other higher-order components.

Let's see a example of how to define a component:

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

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

Higher-Order Components

This is the way you have to extend/add additional functionality to a function component. Rumext exposes two:

  • mf/wrap-memo: same functionality as mf/memo in class based components.

In order to use the high-order components, you need wrap the component manually or passing it as a special property in the metadata:

(mf/defc title
  {::mf/wrap [mf/wrap-memo]}
  [props]
  [:div {:class "label"} (:name props)])

Hooks (React Hooks)

React hooks is a basic primitive that React exposes for add state and side-effects to functional components. Rumext exposes right now only three hooks with a ClojureScript based api.

use-state (React.useState)

Hook used for maintain a local state and in functional components replaces the mf/local mixin. Calling mf/use-state returns an atom-like object that will deref to the current value and you can call swap! and reset! on it for modify its state.

Any mutation will schedule the component to be rerendered.

(require '[rumext.alpha as mf])

(mf/defc local-state
  [props]
  (let [local (mf/use-state 0)]
    [:div {:on-click #(swap! local inc)}
      [:span "Clicks: " @local]]))

(mf/mount (mf/element local-state) js/document.body)

use-ref (React.useRef)

In the same way as use-state returns an atom like object. The unique difference is that updating the ref value does not schedules the component to rerender.

use-effect (React.useEffect)

This is a primitive that allows incorporate probably efectful code into a functional component:

(mf/defc local-timer
  [props]
  (let [local (mf/use-state 0)]
    (mf/use-effect
     {:fn (fn []
            (let [sem (js/setInterval #(swap! local inc) 1000)]
              #(js/clearInterval sem)))})
    [:div "Counter: " @local]))

(mf/mount (mf/element local-state) js/document.body)

The :fn callback will be called once the component mounts (like did-mount) and the returned function callback will be caled once the component will unmounts (like will-unmount). Returning function is optional.

There are an excepction when you pass a :deps parameter, that can change how many times :fn will be executed:

  • :deps nil the default behavior explained before.
  • :deps true the :fn callback will be executed after each render (a combination of did-mount and did-update class based components).
  • :deps (mf/deps variable1 variable2): only execute :start when some of referenced variables changes.
  • :watch [] the same as :watch nil.

NOTE: for avoid vector to js-array conversion, you can pass directly a js array in :deps.

use-memo (React.useMemo)

The purpose of this hook is return a memoized value.

Example:

(mf/defc sample-component
  [{:keys [x]}]
  (let [v (mf/use-memo {:fn #(pow x 10)
                        :deps (mf/deps x)})]
    [:span "Value is:" v]))

On each render, while x has the same value, the v only will be calculated once.

deref

This is a custom hook, alternative to mf/reactive mixin on class components.

The purpose of this hook is reactivelly rerender component on a reference (atom-like object) changes.

Example:

(def clock (atom (.getTime (js/Date.))))
(js/setInterval #(reset! clock (.getTime (js/Date.))) 160)

(mf/defc timer
  [props]
  (let [ts (mf/deref clock)]
    [:div "Timer (deref)" ": "
     [:span ts]]))

Raw Hooks

In some circumstances you will want access to the raw react hooks functions. For this purpose, rumext exposes the following functions: useState, useRef, useMemo and useEffect.

License

Licensed under Eclipse Public License (see LICENSE).

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close