Rehook
ClojureScript React library enabling data-driven architecture
Install / Use
/learn @wavejumper/RehookREADME
rehook
Clojurescript React library enabling data-driven architecture
About
rehook is built from small, modular blocks - each with an explicit notion of time, and a data-first design.
The core library does two things:
- marry React hooks with Clojure atoms
- avoids singleton state
Its modular design, and guiding philosophy has already enabled some rich tooling like rehook-test.
Example apps
- cljspad
- reax-synth -- react native oscillator (demos re-frame like abstractions, integrant, etc)
- todomvc
- rehook-test
Installation
The documentation assumes you are using shadow-cljs.
You will need to provide your own React dependencies, eg:
npm install --save react
npm install --save react-dom
Libraries
- rehook/core - base state/effects fns
- rehook/dom - hiccup templating DSL
- rehook/test - test library
To include one of the above libraries, add the following to your dependencies:
[rehook/core "2.1.11"]
To include all of them:
[rehook "2.1.11"]
Usage
rehook.core
If you need a primer on React hooks, the API docs are a good start.
rehook.core exposes 5 useful functions for state and effects:
use-stateconvenient wrapper overreact/useStateuse-effectconvenient wrapper overreact/useEffectuse-atomuse a Clojure atom (eg, for global app state) within a componentuse-atom-pathlikeuse-atom, except for a path into a atom (eg,get-in)use-atom-fnprovide custom getter/setter fns to build your own abstractions
Usage
(ns demo
(:require
[rehook.core :as rehook]
[rehook.dom :refer-macros [defui]]
[rehook.dom.browser :as dom.browser]
["react-dom" :as react-dom]))
(defn system [] ;; <-- system map (this could be integrant, component, etc)
{:state (atom {:missiles-fired? false})})
(defui my-component
[{:keys [state]} ;; <-- context map from bootstrap fn
props] ;; <-- any props passed from parent component
(let [[curr-state _] (rehook/use-atom state) ;; <-- capture the current value of the atom
[debug set-debug] (rehook/use-state false) ;; <-- local state
[missiles-fired? set-missiles-fired] (rehook/use-atom-path state [:missiles-fired?])] ;; <-- capture current value of path in atom
(rehook/use-effect
(fn []
(js/console.log (str "Debug set to " debug)) ;; <-- the side-effect invoked after the component mounts and debug's value changes
(constantly nil)) ;; <-- the side-effect to be invoked when the component unmounts
[debug])
[:section {}
[:div {}
(if debug
[:span {:onClick #(set-debug false)} "Hide debug"]
[:span {:onClick #(set-debug true)} "Show debug"])
(when debug
(pr-str curr-state))]
(if missiles-fired?
[:div {} "Missiles have been fired!"]
[:div {:onClick #(set-missiles-fired true)} "Fire missiles"])]))
;; How to render a component to the DOM
(react-dom/render
(dom.browser/bootstrap
(system) ;; <-- context map
identity ;; <-- context transformer
clj->js ;; <-- props transformer
my-component) ;; <-- root component
(js/document.getElementById "myapp"))
Hooks gotchas
- When using
use-effect, make sure the values ofdepspass JavaScript's notion of equality! Solution: use simple values instead of complex maps. - Enforced via convention, React hooks and effects need to be defined at the top-level of your component (and not bound conditionally)
Components
rehook.dom
rehook.dom provides hiccup syntax.
rehook.dom provides a baggage free way to pass down application context (eg, integrant or component) as you will see below.
defui
rehook.dom/defui is a macro used to define rehook components. This macro is only syntactic sugar, as all rehook components are cljs fns.
defui takes in two arguments:
context: immutable, application contextprops: any props passed to the component
It must return valid hiccup.
(ns demo
(:require [rehook.dom :refer-macros [defui]]))
(defui my-component [{:keys [dispatch]} _]
[:text {:onClick #(dispatch :fire-missiles)} "Fire missiles!"])
The anonymous counterpart is rehook.dom/ui
fragments
Use the :<> shorthand:
(defui fragmented-ui [_ _]
[:<> {} [:div {} "Div 1"] [:div {} "Div 2"]])
rehook components
Reference the component directly:
(defui child [_ _]
[:div {} "I am the child"])
(defui parent [_ _]
[child])
ReactJS components
Same as rehook components. Reference the component directly:
(require '["react-select" :as ReactSelect])
(defui select [_ props]
[ReactSelect props])
reagent components
(require '[reagent.core :as r])
(defn my-reagent-component []
[:div {} "I am a reagent component, I guess..."])
(defui my-rehook-component [_ _]
[(r/reactify-component my-reagent-component)])
children
;; acceptable
[:div {}
(for [item items]
[item {}])]
;; also acceptable
[:div {}
[child1]
[child2]]
Working with children in rehook components
(require '[rehook.util :as util])
(defui parent [_ props]
[:div {}
(for [child (util/child-seq props)]
[child {:onClick #(js/alert "Extra props merged into child!")}])])
...
[parent {}
[:div {:style {:color "pink"}} "I am a child"]]
hiccup-free
You can opt-out of hiuccup templating by passing a third argument (the render fn) to defui:
(defui no-html-macro [_ _ $]
($ :div {} "rehook-dom without hiccup!"))
Because the $ render fn is passed into every rehook component you can overload it -- or better yet create your own custom templating syntax!
Props
A props transformation fn is passed to the initial bootstrap fn. The return value of this fn must be a JS object.
A good default to use is cljs.core/clj->js.
If you want to maintain Clojure idioms, a library like camel-snake-kebab could be used to convert keys in your props (eg, on-press to onPress)
Props transformation is used for interop with vanilla React components. Therefore, all props passed into rehook do not go through the transformation fn, and remain untouched.
If you need to access the React props in Rehook components (for example, to access children), the JS props computed by React are available as metadata on the props map, under the :react/props key.
You can use the util fn rehook.util/react-props to conveniently extract the React props.
Initializing
react-dom
You can call react-dom/render directly, and bootstrap your component:
(ns example.core
(:require
[example.components :refer [app]]
[rehook.dom.browser :as dom]
["react-dom" :as react-dom]))
(defn system []
{:dispatch (fn [& _] (js/console.log "TODO: implement dispatch fn..."))})
(defn main []
(react-dom/render
(dom/bootstrap (system) identity clj->js app)
(js/document.getElementById "app")))
react-native
You can use the rehook.dom.native/component-provider fn if you directly call AppRegistry
(ns example.core
(:require
[rehook.dom :refer-macros [defui]]
[rehook.dom.native :as dom]
["react-native" :refer [AppRegistry]]))
(defui app [{:keys [dispatch]} _]
[:Text {:onPress #(dispatch :fire-missiles)} "Fire missiles!"])
(defn system []
{:dispatch (fn [& _] (js/console.log "TODO: implement dispatch fn..."))})
(defn main []
(.registerComponent AppRegistry "my-app" (dom/component-provider (system) app)))
Alternatively, if you don't have access to the AppRegistry, you can use the rehook.dom.native/bootstrap fn instead - which will return a valid React element
Context transformer
The context transformer can be incredibly useful for instrumentation, or for adding additional abstractions on top of the library (eg implementing your own data flow engine ala domino)
For example:
(require '[rehook.util :as util])
(defn ctx-transformer [ctx component]
(update ctx :log-ctx #(conj (or % []) (util/display-name component))))
(dom/component-provider (system) ctx-transformer clj->js app)
In this example, each component will have the hierarchy of its parents in the DOM tree under the key :log-ctx.
This can be incredibly useful context to pass to your logging/metrics library!
Linting / editor integration
cursive
rehook.dom/defui-- resolve as defn, indentation asindentrehook.dom/ui-- resolve as fn, indentation asindentrehook.test/defuitest-- resolve as defn, indentation asindentrehook.test/initial-render-- indentation as1rehook.test/next-render-- indentation as1rehook.test/io-- indentation as1rehook.test/is-- indentation as1
cljfmt
Add this to your cljfmt config:
{rehook.dom/defui [[:inner 0]]
rehook.dom/ui [[:inner 0]]}
clj-kondo (calva/etc)
Add this to your .clj-kondo/config.edn file:
{:lint-as {rehook.dom/defui clojure.core/defn
rehook.dom/ui clojure.core/fn}}
Testing
rehook allows you to test your entire application - from data layer to view.
Ho
