rumext
This is a friendly fork of rum.
Using rumext
Add to deps.edn:
funcool/rumext {:mvn/version "2.0.0-SNAPSHOT"}
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])
(def title
(mf/fnc title [{:keys [name]}]
[:div {:class "label"} name]))
The fnc
is a fn
analogous macro for creating function
components. There are also defc
macro that behaves in the similar
way to the defn
:
(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-reactive
: same functionality asmf/reactive
in class based components. -
mf/wrap-memo
: same functionality asmf/memo
in class based components.
And you can use them in two ways, the traditional one that consists in direct wrapping a component with an other:
(def title
(mf/wrap-memo
(mf/fnc title [{:keys [name]}]
[:div {:class "label"} name])))
Or using a special metadata syntax, that does the same thing but with less call ceremony:
(mf/defc title
{:wrap [mf/wrap-memo]}
[props]
[:div {:class "label"} (:name props)])
NOTE: The mf/reactive
higher-order component behind the scenes uses
React Hooks as internal primitives for implement the same behavior
as the mf/reactive
mixin on class components.
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
:start (fn [] (js/setInterval #(swap! local inc) 1000))
:end (fn [sem] (js/clearInterval sem)))
[:div "Counter: " @local]))
(mf/mount (mf/element local-state) js/document.body)
The :start
callback will be called once the component mounts (like
did-mount
) and the :end
callback will be caled once the component
will unmounts (like will-unmount
).
There are an excepction when you pass a :deps
parameter, that can
change how many times :start
will be executed:
-
:deps nil
the default behavior explained before. -
:deps true
the:start
callback will be executed after each render (a combination ofdid-mount
anddid-update
class based components). -
: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 {:init #(pow x 10)
: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/wrap-reactive
&
mf/react
.
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).