{"id":15873891,"url":"https://github.com/wavejumper/rehook","last_synced_at":"2025-05-11T10:26:31.259Z","repository":{"id":36083118,"uuid":"217793237","full_name":"wavejumper/rehook","owner":"wavejumper","description":"ClojureScript React library enabling data-driven architecture","archived":false,"fork":false,"pushed_at":"2023-05-07T03:12:31.000Z","size":215,"stargazers_count":78,"open_issues_count":4,"forks_count":2,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-03-31T21:51:28.962Z","etag":null,"topics":["clojurescript","hiccup","react","react-hooks"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wavejumper.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-10-27T02:05:40.000Z","updated_at":"2025-02-22T19:00:20.000Z","dependencies_parsed_at":"2025-01-03T12:21:56.690Z","dependency_job_id":null,"html_url":"https://github.com/wavejumper/rehook","commit_stats":{"total_commits":129,"total_committers":6,"mean_commits":21.5,"dds":"0.20155038759689925","last_synced_commit":"11f6f29951f599cb696e4f2f066489e09c62cbea"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wavejumper%2Frehook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wavejumper%2Frehook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wavejumper%2Frehook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wavejumper%2Frehook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wavejumper","download_url":"https://codeload.github.com/wavejumper/rehook/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253549311,"owners_count":21925919,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojurescript","hiccup","react","react-hooks"],"created_at":"2024-10-06T01:08:05.468Z","updated_at":"2025-05-11T10:26:31.228Z","avatar_url":"https://github.com/wavejumper.png","language":"Clojure","readme":"# rehook\n\n[![Clojars Project](https://img.shields.io/clojars/v/rehook.svg)](https://clojars.org/rehook)\n[![CircleCI](https://circleci.com/gh/wavejumper/rehook.svg?style=svg)](https://circleci.com/gh/wavejumper/rehook)\n\nClojurescript React library enabling data-driven architecture\n\n## About\n\nrehook is built from small, modular blocks - each with an explicit notion of time, and a data-first design.\n\nThe core library does two things:\n\n* marry React hooks with Clojure atoms\n* avoids singleton state\n\nIts modular design, and guiding philosophy has already enabled some rich tooling like [rehook-test](#testing).\n\n## Example apps\n\n* [cljspad](https://cljspad.dev)\n* [reax-synth](https://github.com/wavejumper/reax/tree/master/examples/synth) -- react native oscillator (demos re-frame like abstractions, integrant, etc)\n* [todomvc](https://github.com/wavejumper/rehook/tree/master/examples/todomvc)\n* [rehook-test](https://github.com/wavejumper/rehook/tree/master/rehook-test)\n\n## Installation\n\nThe documentation assumes you are using [shadow-cljs](https://shadow-cljs.org/).\n\nYou will need to provide your own React dependencies, eg:\n```\nnpm install --save react\nnpm install --save react-dom\n```\n\n### Libraries\n\n* rehook/core - base state/effects fns\n* rehook/dom - hiccup templating DSL\n* rehook/test - test library\n\nTo include one of the above libraries, add the following to your dependencies:\n\n```\n[rehook/core \"2.1.11\"]\n```\n\nTo include all of them:\n\n```clojure\n[rehook \"2.1.11\"]\n```\n\n## Usage\n\n## rehook.core\n\nIf you need a primer on React hooks, the [API docs](https://reactjs.org/docs/hooks-reference.html) are a good start.\n\n`rehook.core` exposes 5 useful functions for state and effects:\n\n- `use-state` convenient wrapper over `react/useState`\n- `use-effect` convenient wrapper over `react/useEffect`\n- `use-atom` use a Clojure atom (eg, for global app state) within a component\n- `use-atom-path` like `use-atom`, except for a path into a atom (eg, `get-in`)\n- `use-atom-fn` provide custom getter/setter fns to build your own abstractions\n\n## Usage\n\n```clojure\n(ns demo\n  (:require\n    [rehook.core :as rehook]\n    [rehook.dom :refer-macros [defui]]\n    [rehook.dom.browser :as dom.browser]\n    [\"react-dom\" :as react-dom]))\n\n(defn system [] ;; \u003c-- system map (this could be integrant, component, etc)\n  {:state (atom {:missiles-fired? false})})\n\n(defui my-component\n  [{:keys [state]} ;; \u003c-- context map from bootstrap fn\n   props] ;; \u003c-- any props passed from parent component\n  (let [[curr-state _]                       (rehook/use-atom state) ;; \u003c-- capture the current value of the atom\n        [debug set-debug]                    (rehook/use-state false) ;; \u003c-- local state\n        [missiles-fired? set-missiles-fired] (rehook/use-atom-path state [:missiles-fired?])] ;; \u003c-- capture current value of path in atom\n\n    (rehook/use-effect\n      (fn []\n        (js/console.log (str \"Debug set to \" debug)) ;; \u003c-- the side-effect invoked after the component mounts and debug's value changes\n        (constantly nil)) ;; \u003c-- the side-effect to be invoked when the component unmounts\n      [debug])\n\n    [:section {}\n      [:div {}\n        (if debug\n          [:span {:onClick #(set-debug false)} \"Hide debug\"]\n          [:span {:onClick #(set-debug true)} \"Show debug\"])\n        (when debug\n          (pr-str curr-state))]\n\n      (if missiles-fired?\n        [:div {} \"Missiles have been fired!\"]\n        [:div {:onClick #(set-missiles-fired true)} \"Fire missiles\"])]))\n\n;; How to render a component to the DOM\n(react-dom/render\n  (dom.browser/bootstrap\n    (system) ;; \u003c-- context map\n    identity ;; \u003c-- context transformer\n    clj-\u003ejs ;; \u003c-- props transformer\n    my-component) ;; \u003c-- root component\n  (js/document.getElementById \"myapp\"))\n```\n\n### Hooks gotchas\n\n* When using `use-effect`, make sure the values of `deps` pass JavaScript's notion of equality! Solution: use simple values instead of complex maps.\n* Enforced via convention, React hooks and effects need to be defined at the top-level of your component (and not bound conditionally)\n\n# Components\n\n## rehook.dom\n\n`rehook.dom` provides hiccup syntax.\n\n`rehook.dom` provides a baggage free way to pass down application context (eg, [integrant](https://github.com/weavejester/integrant) or [component](https://github.com/stuartsierra/component)) as you will see below.\n\n## defui\n\n`rehook.dom/defui` is a macro used to define `rehook` components. This macro is only syntactic sugar, as all `rehook` components are cljs fns.\n\n`defui` takes in two arguments:\n\n* `context`: immutable, application context\n* `props`: any props passed to the component\n\nIt must return valid hiccup.\n\n```clojure\n(ns demo\n  (:require [rehook.dom :refer-macros [defui]]))\n\n(defui my-component [{:keys [dispatch]} _]\n  [:text {:onClick #(dispatch :fire-missiles)} \"Fire missiles!\"])\n```\n\nThe anonymous counterpart is `rehook.dom/ui`\n\n### fragments\n\nUse the `:\u003c\u003e` shorthand:\n\n```clojure\n(defui fragmented-ui [_ _]\n  [:\u003c\u003e {} [:div {} \"Div 1\"] [:div {} \"Div 2\"]])\n```\n\n### rehook components\n\nReference the component directly:\n\n```clojure\n(defui child [_ _]\n  [:div {} \"I am the child\"])\n\n(defui parent [_ _]\n  [child])\n```\n\n### ReactJS components\n\nSame as rehook components. Reference the component directly:\n\n```clojure\n(require '[\"react-select\" :as ReactSelect])\n\n(defui select [_ props]\n  [ReactSelect props])\n```\n\n### reagent components\n\n```clojure\n(require '[reagent.core :as r])\n\n(defn my-reagent-component []\n  [:div {} \"I am a reagent component, I guess...\"])\n\n(defui my-rehook-component [_ _]\n  [(r/reactify-component my-reagent-component)])\n```\n\n### children\n\n```clojure\n;; acceptable\n[:div {}\n  (for [item items]\n    [item {}])]\n\n;; also acceptable\n[:div {}\n  [child1]\n  [child2]]\n```\n\n#### Working with children in rehook components\n\n```clojure\n(require '[rehook.util :as util])\n\n(defui parent [_ props]\n  [:div {} \n    (for [child (util/child-seq props)]\n      [child {:onClick #(js/alert \"Extra props merged into child!\")}])])\n\n...\n\n[parent {} \n  [:div {:style {:color \"pink\"}} \"I am a child\"]]\n```\n\n### hiccup-free\n\nYou can opt-out of hiuccup templating by passing a third argument (the render fn) to `defui`:\n\n```clojure\n(defui no-html-macro [_ _ $]\n  ($ :div {} \"rehook-dom without hiccup!\"))\n```\n\nBecause the `$` render fn is passed into every rehook component you can overload it -- or better yet create your own custom templating syntax!\n\n## Props\n\nA props transformation fn is passed to the initial `bootstrap` fn. The return value of this fn must be a JS object.\n\nA good default to use is `cljs.core/clj-\u003ejs`.\n\nIf you want to maintain Clojure idioms, a library like [camel-snake-kebab](https://github.com/clj-commons/camel-snake-kebab) could be used to convert keys in your props (eg, `on-press` to `onPress`)\n\nProps 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.\n\nIf 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.\n\nYou can use the util fn `rehook.util/react-props` to conveniently extract the React props.\n\n## Initializing\n\n## react-dom\n\nYou can call `react-dom/render` directly, and `bootstrap` your component:\n\n```clojure\n(ns example.core\n  (:require\n    [example.components :refer [app]]\n    [rehook.dom.browser :as dom]\n    [\"react-dom\" :as react-dom]))\n\n(defn system []\n  {:dispatch (fn [\u0026 _] (js/console.log \"TODO: implement dispatch fn...\"))})\n\n(defn main []\n  (react-dom/render\n    (dom/bootstrap (system) identity clj-\u003ejs app)\n    (js/document.getElementById \"app\")))\n```\n\n## react-native\n\nYou can use the `rehook.dom.native/component-provider` fn if you directly call [AppRegistry](https://facebook.github.io/react-native/docs/appregistry)\n\n```clojure\n(ns example.core\n  (:require\n    [rehook.dom :refer-macros [defui]]\n    [rehook.dom.native :as dom]\n    [\"react-native\" :refer [AppRegistry]]))\n\n(defui app [{:keys [dispatch]} _]\n  [:Text {:onPress #(dispatch :fire-missiles)} \"Fire missiles!\"])\n\n(defn system []\n  {:dispatch (fn [\u0026 _] (js/console.log \"TODO: implement dispatch fn...\"))})\n\n(defn main []\n  (.registerComponent AppRegistry \"my-app\" (dom/component-provider (system) app)))\n```\n\nAlternatively, 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\n\n## Context transformer\n\nThe 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](https://domino-clj.github.io/))\n\nFor example:\n\n```clojure\n(require '[rehook.util :as util])\n\n(defn ctx-transformer [ctx component]\n  (update ctx :log-ctx #(conj (or % []) (util/display-name component))))\n\n(dom/component-provider (system) ctx-transformer clj-\u003ejs app)\n```\n\nIn this example, each component will have the hierarchy of its parents in the DOM tree under the key `:log-ctx`.\n\nThis can be incredibly useful context to pass to your logging/metrics library!\n\n## Linting / editor integration\n\n### cursive\n\n* `rehook.dom/defui` -- resolve as defn, indentation as `indent`\n* `rehook.dom/ui` -- resolve as fn, indentation as `indent`\n* `rehook.test/defuitest` -- resolve as defn, indentation as `indent`\n* `rehook.test/initial-render` -- indentation as `1`\n* `rehook.test/next-render` -- indentation as `1`\n* `rehook.test/io` -- indentation as `1`\n* `rehook.test/is` -- indentation as `1`\n\n### cljfmt\n\nAdd this to your cljfmt config:\n\n```clojure\n{rehook.dom/defui [[:inner 0]]\n rehook.dom/ui    [[:inner 0]]}\n```\n\n### clj-kondo (calva/etc)\n\nAdd this to your `.clj-kondo/config.edn` file:\n\n```\n{:lint-as {rehook.dom/defui clojure.core/defn\n           rehook.dom/ui    clojure.core/fn}}\n```\n\n## Testing\n\nrehook allows you to test your entire application - from data layer to view.\n\nHow? Because `rehook` promotes building applications with no singleton global state.\n\nTherefore, you can treat your components as 'pure functions', as all inputs to the component are passed in as arguments.\n\nrehook-test supports:\n\n* server, react-dom and react-native\n* [cljs.test + nodejs target for headless/CI](https://circleci.com/gh/wavejumper/rehook)\n* browser for devcards-like interactive development\n* whatever else you can think of. it's just a function call really.\n\n# rehook-test\n\n## Demo\n\nA demo report generated from rehook's own todomvc tests can be found [here](https://tscrowley.dev/rehook/)\n\n## Screenshots\n\nWrite tests, and get reports like this:\n\n![image](https://i.imgur.com/jAbQXk9.png)\n\nAnd headless node cljs tests!\n\n![image](https://i.imgur.com/35ehUrd.png)\n\n## Time-travel driven development\n\nWriting tests for rehook is not dissimilar to how you might test with [datomic](https://www.datomic.com/) or Kafka's [TopologyTestDriver](https://kafka.apache.org/11/documentation/streams/developer-guide/testing.html), with a bit of [devcards](https://github.com/bhauman/devcards) in the mix.\n\nEach state change produces a snapshot in time that rehook captures as a 'scene'.\n\nLike Kafka's ToplogyTestDriver, the tests run in a simulated library runtime.\n\nHowever, a read-only snapshot of the dom is rendered for each scene (as you can see above)!\n\nThis allows you to catch any runtime errors caused by invalid inputs for each re-render.\n\n## rehook.test API\n\n**Note:** while documentation is improving, please check out the [rehook tests](https://github.com/wavejumper/rehook/tree/master/examples/todomvc/src/test) for a reference on how to use the API.\n\n`rehook.test` wraps the [cljs.test](https://clojurescript.org/tools/testing) API with a bit of additional syntactic sugar.\n\nThis means rehook tests compile to something `cljs.test` understands!\n\n```clojure\n(ns todo-test\n  (:require [rehook.test :as rehook.test :refer-macros [defuitest is io initial-render next-render]]\n            [rehook.demo.todo :as todo]))\n\n(defuitest todo-app--clear-completed\n  [[scenes ctx] {:system      todo/system\n                 :system-args []\n                 :shutdown-f  identity\n                 :ctx-f       identity\n                 :props-f     identity\n                :component   todo/todo-app}]\n\n  (-\u003e (initial-render scenes\n        (is \"Initial render should show 4 TODO items\"\n          (= (rehook.test/children :clear-completed) [\"Clear completed \" 4]))\n\n        (io \"Click 'Clear completed'\"\n          (rehook.test/invoke-prop :clear-completed :onClick [{}])))\n\n      (next-render\n       (is \"After clicking 'Clear Completed', there should be no TODO items\"\n         (nil? (rehook.test/children :clear-completed)))))\n```\n\nThe `-\u003e` threading macro is used to chain our tests.\n\nWriting tests consists of using two basic primitives:\n\n* `rehook.test/io` - wrapping any side-effects that will trigger a re-render (such as DOM events, HTTP calls, etc)\n* `rehook.test/is` - like `cljs.test/is`, this is how you write assertions for the current render\n\nEach test body (consisting of `is` and `io`) is scoped to a 'snapshot' of a render:\n\n* `rehook.test/initial-render` - called on our first test\n* `rehook.test/next-render` - trigger a re-render by playing any effects\n\n## defuitest\n\n`rehook.test/defuitest` takes in a map describing your application:\n\n```clojure\n{:system      todo/system ;; \u003c-- your ctx constructor, eg ig/init\n :system-args [] ;; \u003c-- any arguments to your ctx constructor\n :shutdown-f  identity ;; \u003c-- called after the test has finished, eg ig/halt!\n :ctx-f       identity ;; \u003c-- likely the same ctx-f passed into your applications bootstrap call\n :props-f     identity ;; \u003c-- likely the same props-f passed into your applications bootstrap call\n :component   todo/todo-app}] ;; \u003c-- the rehook component your are writing tests for\n```\n\n## Instrumenting the DOM\n\nAdd a **unique** key named `:rehook/id` to the props of any component you want to instrument:\n\n```clojure\n[:div {:rehook/id :my-unique-key} \"I will be instrumented!\"]\n```\n\nNote: this key gets compiled out when running outside of `rehook.test`!\n\nYou can then invoke props and view the props and children using the following fns:\n\n* `rehook.test/children` - returns a collection of children\n* `rehook.test/get-prop` - returns the props of the component\n* `rehook.test/invoke-prop` - invokes a component's event (eg, `onClick`)\n\nYou can see these three fns in action in the demo code above.\n\n**TODO:** provide an easy way to construct mock JS events. Perhaps look to using [jsdom](https://github.com/jsdom/jsdom)?\n\n## Testing the data layer\n\n* The test reports provides a way to view effects and state over time. However, this is provided only as a means of debugging. Both `use-state` and `use-effects` are implementation details - and shouldn't be tested.\n* Therefore, `rehook-test` is about testing the resulting output of the component.\n* If you follow a re-frame like pattern of using global app state, it should be possible to inspect your subscriptions and invoke your effects using the `rehook.test` primitives. More documentation to follow.\n\n## rehook.test reports\n\n**Note**: the graphical test reporter only works for `react-dom` tests. It would be great to implement something similar for React Native (using the simulator, expo web preview, etc)!\n\nCreate a build in your `shadow-cljs.edn` file like so:\n\n```clojure\n  {:target :browser\n   :output-dir \"public/js\"\n   :asset-path \"/js\"\n   :closure-defines {rehook.test.browser/HTML \"\u003c!DOCTYPE html\u003e\u003chtml\u003e\u003chead\u003e\u003clink rel=\\\"stylesheet\\\" href=\\\"styles/todo.css\\\"\u003e\u003c/head\u003e\u003cbody\u003e\u003cdiv\u003e\u003c/div\u003e\u003c/body\u003e\u003c/html\u003e\" ;; optional, the initial DOM html (eg, the index.html of your actual app)\n                     rehook.test/target \"app\" ;; optional, the div id where rehook's report renders\n                     rehook.test/domheight 400} ;; optional, the dom preview's iframe height\n   :devtools {:before-load rehook.test/clear-registry!} ;; add this if using hot reload\n   :modules {:main {:entries [rehook.test.browser\n                              todo-test] ;; \u003c-- your test nses go here...\n                    :init-fn rehook.test.browser/report}}}\n```\n\nAnd you are done!\n\n```\nshadow-cljs watch :my-build-id\n```\n\nWill render your test report. As you update your test/application code, the report will also update!\n\nInside of the folder `dev-http` serves, add a `report.html` file like [this](https://github.com/wavejumper/rehook/blob/master/examples/todomvc/public/report.html) one.\n\n## rehook.test headless\n\nAdd a build in your `shadow-cljs.edn` file like so:\n\n```\n{:target    :node-test\n :output-to \"out/test.js\"}\n```\n\nAnd you are done!\n\n```\nshadow-cljs compile :my-build-id\nnode out/test.js\n```\n\nWill run your headless tests\n\n## rehook.test TODOs\n\n* Better feedback when things don't go as expected (eg, `io` call didn't cause a re-render)\n* I want Github-level diffs between the previous scene and the next scene's hiccup. [react-diff-viewer](https://github.com/praneshr/react-diff-viewer)?\n* How can we use clojure spec and perhaps property based testing to put this thing on steroids? Eg, instrument and render shrunk result\n* This tool could be used to instrument running app? Eg, reframe10x but on even more steroids :)\n* This tool **could** lint/detect various warnings/runtime problems. Eg, when a :key on a component is required, when state/effects are setup incorrectly, etc\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwavejumper%2Frehook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwavejumper%2Frehook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwavejumper%2Frehook/lists"}