{"id":32181487,"url":"https://github.com/lambdam/mook","last_synced_at":"2026-02-21T00:01:59.201Z","repository":{"id":62433691,"uuid":"286477027","full_name":"lambdam/mook","owner":"lambdam","description":"Frontend state mangement library for ClojureScript","archived":false,"fork":false,"pushed_at":"2020-11-28T17:37:26.000Z","size":122,"stargazers_count":10,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-10-19T08:28:26.553Z","etag":null,"topics":["clojurescript","datascript","hooks","react"],"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/lambdam.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"license.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-08-10T13:03:12.000Z","updated_at":"2022-12-15T04:15:28.000Z","dependencies_parsed_at":"2022-11-01T21:01:45.449Z","dependency_job_id":null,"html_url":"https://github.com/lambdam/mook","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/lambdam/mook","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdam%2Fmook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdam%2Fmook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdam%2Fmook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdam%2Fmook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lambdam","download_url":"https://codeload.github.com/lambdam/mook/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdam%2Fmook/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29668627,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-20T23:24:07.480Z","status":"ssl_error","status_checked_at":"2026-02-20T23:24:06.202Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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","datascript","hooks","react"],"created_at":"2025-10-21T22:47:14.013Z","updated_at":"2026-02-21T00:01:59.194Z","avatar_url":"https://github.com/lambdam.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Mook\n\n[![Clojars Project](https://img.shields.io/clojars/v/mook.svg)](https://clojars.org/mook)\n\nMook is a library designed to handle frontend application state(s).  \nIt serves the same purpose than [re-frame](https://github.com/Day8/re-frame) or\n[citrus](https://github.com/clj-commons/citrus).\n\n⚠️**This library is in an experimental state. Depending on the feedbacks that I\nwould receive in the coming weeks, things can change.**  \nBut that's the point: play with the code and send feebacks (Clojurians Slack\n\"mook\" channel or pull requests)! Check the [TodoMVC examples](/examples).\n\nAlso, check the [interactive article](https://lambdam.com/blog/2020-10-mook-bis/)\nthat introduces the library and explains the design decisions.\n\n```clj\n;; Clojure CLI/deps.edn\nmook {:mvn/version \"0.2.0\"}\n\n;; Leiningen/Boot\n[mook \"0.2.0\"]\n```\n\n\n## Design ideas\n\n- Query the state directly and locally in components.\n- Use functions and promises to handle state transitions and async workflows (aka\n  actions, or commands in Mook parlance).\n- Use Ring style middlewares to extend command (~ action) behaviors.\n- Make commands (~ actions) composable.\n- Enable the use of Datascript along with atoms to store state.\n\n\n## The state and its transformation\n\nTraditionally, mutating the global state is done through \"actions\". I chose\nanother semantics after a discussion with a friend\n([@chpill](https://github.com/chpill)): \"commands\". The semantics is taken from\nthe event sourcing architecture that distinguishes \"facts\", things that happened\nfor sure, and \"commands\", sending the intention of a transformation. But this\ncommand can fail for many reasons.\n\nA **command** in mook is a **function that takes a map and returns a promise\nthat resolves to a map**. The promise expresses the fact that the future result\nof a command can be a success or a failure. Also the promise has the useful\nproperty to be chainable.\n\nThis is very similar to an async Ring handler that returns a\n[Manifold](https://github.com/aleph-io/manifold)\n[deferred](https://aleph.io/manifold/deferreds.html) (used with the\n[Aleph](https://github.com/aleph-io/aleph) webserver). And the traditional\nway of extending handlers in Ring, is to use\n**[middlewares](https://github.com/ring-clojure/ring/wiki/Concepts#middleware)**.\n\n\n## Usage\n\nMook introduces the notion of state stores: instead of having one source of\nstate that would fire global re-renders on every little change, it enables\nhaving smaller pieces of state that would fire partial re-renders.\n\nTypically two types of state stores can be used:\n- A Datascript database that would hold state that flows troughout all the\n  architecture (frontend and backend)\n- A Clojure atom that would hold frontend only state and that acts like a\n  lightweight key-value store.\n\n```cljs\n;; Classical one source of truth hashmap\n{:foo ...  ;; \u003c- any change in the hashmap will fire a whole re-render.\n :bar ...}\n\n;; Mook approach with state stores\n{:my.app/local-store {...}             ;; \u003c- changes to local-store re-render only concerned UI parts\n :my.app/app-db \u003cDatascript db value\u003e} ;; \u003c- changes to app-db re-render only concerned UI parts\n```\n\nThis is an optimization meant to fire re-renders only by store. It is useful for\ncomplex Datascript queries that can be costly on every re-render.\n\nThis optimization is a variation around the \"one source of thruth\" concept since\nat every point in time, Mook can give an immutable hashmap of the state where keys\nare the names of the \"sub-states\".\n\n### Setup\n\nNow that we saw (briefly) what state stores, commands and middlewares are in\nMook context, let's glue them together.\n\nMook behavior and storage are configured through middlewares.  \nThere is one mandatory middleware to provide on initialization: the state stores\nmiddleware.  \nThen any other middleware can be added (for http requests, browser local storage\netc...).\n\nExample:\n\n```cljs\n(ns my.app)\n\n(require '[mook.core :as m])\n(require '[promesa.core :as p])\n(require '[datascript.core :as d])\n\n;; Datascript (structured business logic)\n\n(def db-schema {...})\n\n(defonce app-db*\n  (d/create-conn db-schema))\n\n;; Atom (lightweight store)\n\n(defonce local-store*\n  {::current-user-id nil\n   ::in-progress? false\n   ...})\n\n;; State stores middleware. Mandatory!\n(def wrap-state-stores\n  (m/create-state-store-wrapper\n    [{::m/store-key ::local-store*\n      ::m/state-key ::local-store\n      ::m/store*    local-store*}\n     {::m/store-key ::app-db*\n      ::m/state-key ::app-db\n      ::m/store*    app-db*}]))\n\n;; Logging middleware, for the example\n(defn wrap-console-log [command]\n  (fn process-console-log\u003e\u003e [data]\n    (println \"Data before\\n\" data)\n    (-\u003e (command data)\n        (p/then (fn [data']\n                  (println \"Data after\\n\" data')\n                  data')))))\n\n(m/init-mook!\n  {::m/command-middlewares [wrap-state-stores\n                            wrap-console-log\n                            ;; Add as many middlewares as you wish.\n                            ;; They will be applied in the declared order.\n                            ]})\n```\n\n⚠️ Notice how map keys are all namespaced. Mook heavilly uses core.spec and\ndefines specs for almost every value that flows through the architecture.  \nThree values have noticeable semantics:\n- `:mook.core/store*`: the store itself as a reference (the Clojure atom, the\n  Datascript \"connection\"...).\n- `:mook.core/store-key`: the name given to the store.\n- `:mook.core/state-key`: the name of the state contained in a store (~ the\n  dereferenced reference).\n\nFinally we can launch our React application:\n\n```cljs\n(defn root-component [_props]\n  ...)\n\n(js/ReactDOM.render\n  (js/React.createElement root-component nil)\n  (js/document.getElementById \"app-root\"))\n```\n\n__Note: for the time being, Mook stores the state in a singleton. We don't have\nto use React context to expose the stores.__\n\nMook is only about state management. But Mook relies on the Hooks API (React \u003e=\n16.8).  \nFor the view you can use:\n- The bare React library.\n- The very thin wrapper included in Mook. Check the\n  [documentation](/doc/react-wrapper.md).\n- Another hook ready wrapper like [helix](https://github.com/lilactown/helix) or\n  [crinkle](https://github.com/favila/crinkle) (not tested yet).\n\nNow, there are two things that we can do with our application: read data from\nthe state stores and modify the state stores.\n\n### Read the state(s)\n\nMook defines two hooks: `use-mook-state` and `use-param-mook-state`.\n\nMook hooks have two arities: the unary one that accepts a map with all\nparameters explicitly given. In a way, this arity acts like labelled arguments\nin other languages (like OCaml for example). Respectively the binary and ternary\narities with positional arguments act like shorthand versions of the function\ncall.\n\n`use-mook-state` takes a state store name and a handler. The handler receives\nthe dereferenced store (~ the state) as its first and only parameter. There are\nonly two ways for this hook to fire a re-render:\n1. the result of the handler changes (the handler might close over changing\nvalues)\n2. the state store changes and the result of the previous known handler changes.\n\n```cljs\n(require '[mook.core :as m])\n\n;; Arity 1\n(use-mook-state {::m/state-key ::local-store\n                 ::m/handler (fn [state]\n                               (::current-user-id state))})\n\n;; Arity 2 (shorthand)\n(use-mook-state ::local-store ::current-user-id)\n```\n\nA more evolved one (`use-param-mook-state`), similar to React behaviour with\ncomponent `key` attibute, where the developper controls the data that will\nprovoque a new comparison. This hook was crafted to address the fact that\ncomplex queries in Datascript might be slow, and we don't want it to replay on\nevery functional component call. Also this hook fires a re-render when the \"key\"\nvalue changes or that the result of a new state of the store changes.\n\n```cljs\n(require '[mook.core :as m])\n\n;; Arity 1\n(use-param-mook-state {::m/state-key ::app-db\n                       ::m/params [current-user-id book-ids]\n                       ::m/handler (fn [db] ...)})\n\n;; Arity 3 (shorthand)\n(use-param-mook-state ::app-db\n                      [current-user-id book-ids]\n                      (fn [db] ...))\n```\n\n### Mook commands (~ actions)\n\n⚠️For the time being and since Mook is in an early stage, there are two ways of\ntransforming the states:\n- By accessing the store references directly and transforming then directly in\n  the commands.  \n  This is the original Mook approach.\n- By declaring a new value of a given store in the returned value of a command.  \n  This approach is based on the feebacks from\n  [@vvvvalvalval](https://github.com/vvvvalvalval) that favors considering the\n  state as big immutable value without sacrificing the optimization of partial\n  re-renders of the state (the state stores).\n\nTo access mook store context and behaviors defined in the middlewares, a command\nhas to be wrapped with mook middlewares.\n\nThis can be done statically in a namespace or dynamically in a React handlers.\n\n```cljs\n(require '[mook.core :as m])\n(require '[promesa.core :as p])\n\n\n;; The command\n(defn create-new-todo\u003e\u003e [data]\n  ...)\n\n;; We can spec it! It is a regular function.\n(s/fdef create-new-todo\u003e\u003e\n  :args (s/cat :data ...)\n  :ret p/promise?)\n\n;; Finally we wrap it so that it will receive the stores in its\n;; parameters (and any other thing defined in the middlewares).\n(def \u003cset-route\u003e\u003e\n  (m/wrap set-route\u003e\u003e))\n```\n\nNotice the convention here. `...\u003e\u003e` indicates that the function returns a\npromise. `\u003c...\u003e` indicated that the function has been wrapped with Mook\nmiddlewares.\n\n### State store middleware, version 1: direct reference access\n\nThe state store middleware merges all the states and stores in the data provided\nto a command. In our case, for the input: `{:foo \"bar\"}`, the command will\nreceive the following map:\n\n```cljs\n{:foo \"bar\"\n ::local-store {...}\n ::local-store* \u003cAtom ...\u003e\n ::app-db #datascript/DB{...}\n ::app-db* \u003cDB connection ...\u003e\n }\n```\n\nThis would be a command definition:\n\n```cljs\n(require '[mook.core :as m])\n(require '[datascript.core :as d])\n(require '[promesa.core :as p])\n\n;; The command\n(defn create-new-todo\u003e\u003e [{::keys [app-db* local-store*] :as data}]\n  (let [title (:todo/title data)]\n    (d/transact! app-db*\n                 [{:todo/title title\n                   :todo/completed? false\n                   :todo/created-at (js/Date.)}])\n    (swap! local-store* assoc ::latest-todo title)\n    (p/resolved (dissoc data :todo/title))))\n\n(defn set-route\u003e\u003e [{::keys [local-store*] :as data}]\n  (swap! local-store* merge (select-keys data [::current-route]))\n  (p/resolved (dissoc data ::current-route)))\n\n;; We can spec it! It is a regular function.\n(s/fdef create-new-todo\u003e\u003e\n  :args (s/cat :data (s/keys :req [::local-store* ::app-db* :todo/title]))\n  :ret p/promise?)\n\n;; Finally we wrap it so that it will receive the stores in its\n;; parameters (and any other thing defined in the middlewares).\n(def \u003ccreate-new-todo\u003e\u003e\n  (m/wrap set-route\u003e\u003e))\n```\n\nOne last mandatory setup is to implement a `Watchable` protocol for all\nreferences so that Mook can fire re-renders on state transitions. It is already\nimplemented for Clojure atoms but not for Datascript databases since it is not a\nmandatory dependency.\n\n```cljs\n(require '[mook.core :as m])\n(require 'datascript.db')\n\n(extend-type datascript.db/DB\n  m/Watchable\n  (m/listen! [this key f]\n    (d/listen! this key (fn watch-changes [{:keys [db-after] :as _transaction-data}]\n                          (f {::m/new-state db-after}))))\n  (m/unlisten! [this key]\n    (d/unlisten! this key)))\n```\n\nTake a look at:\n- The [todomvc-direct-ref-mutations](/examples/todomvc-direct-ref-mutations) example.\n- Its [store](/examples/todomvc-direct-ref-mutations/src/todomvc/stores.cljs) definition.\n- Its definitions of the [commands](/examples/todomvc-direct-ref-mutations/src/todomvc/commands.cljs).\n\n### State store middleware, version 2: declarative mutations\n\nIf we want to use the declarative approach, we have... nothing to do.\n\nThe command will receive the same keys but we can only use the state values\n(that are immutable values).\n\nThe state store middleware merges all the states and stores in the data provided\nto a command. In our case, for the input: `{:foo \"bar\"}`, the command will\nreceive the following map:\n\n```cljs\n{:foo \"bar\"\n ::local-store {...}\n ::local-store* \u003cAtom ...\u003e ;; \u003c- Present but useless\n ::app-db #datascript/DB{...}\n ::app-db* \u003cDB connection ...\u003e ;; \u003c- Present but useless\n }\n```\n\nThis would be a command definition:\n\n```cljs\n(require '[mook.core :as m])\n(require '[datascript.core :as d])\n(require '[promesa.core :as p])\n\n;; The command\n(defn create-new-todo\u003e\u003e [{::keys [app-db local-store] :as data}]\n  (let [title (:todo/title data)\n        new-app-db (d/db-with app-db\n                              [{:todo/title title\n                                :todo/completed? false\n                                :todo/created-at (js/Date.)}])\n        new-local-store (assoc local-store\n                               ::latest-todo\n                               title)]\n    (p/resolved\n      (-\u003e data\n          (dissoc :todo/title)\n          (assoc ::m/state-transitions [{::m/state-key ::app-db\n                                         ::m/new-state new-app-db}\n                                        {::m/state-key ::local-store\n                                         ::m/new-state new-local-store}])))))\n\n;; We can spec it! It is a regular function.\n(s/fdef create-new-todo\u003e\u003e\n  :args (s/cat :data (s/keys :req [::local-store ::app-db :todo/title]))\n  :ret p/promise?)\n\n;; Finally we wrap it so that it will receive the stores in its\n;; parameters (and any other thing defined in the middlewares).\n(def \u003ccreate-new-todo\u003e\u003e\n  (m/wrap create-new-todo\u003e\u003e))\n```\n\nTake a look at:\n- The [todomvc-declarative-mutations](/examples/todomvc-declarative-mutations) example.\n- Its [store](/examples/todomvc-declarative-mutations/src/todomvc/stores.cljs) definition.\n- Its definitions of the [commands](/examples/todomvc-declarative-mutations/src/todomvc/commands.cljs).\n\n### Commmands: subtle differences\n\nBy taking a close look at two versions of the same command\n(`create-new-todo\u003e\u003e`), we can see that:\n- Direct ref version: destructured keys and spec use the \"star\" version of the\n  state stores: `::local-store*` and `::app-db*`. This convention indicates the\n  use of the reference itself (aka **the store**).\n- Declarative version: destructured keys and spec use the \"starless\" version of\n  the state store: `::local-store` and `::app-db`. This convention indicates the\n  dereferenced version of the state stores, and thus immutable values (aka **the\n  state**)\n\n### Mook commands advantage\n\nAn interesting thing with this approach is that local commands and global\ncommands can be coordinated easily. There is an example of this in the\n[introductory article](https://lambdam.com/blog/2020-09-mook/#mook-book-app) (in\nthe `onClick` handler of the `book-detail` component). There is another one in\nthe [TodoMVC\nexamples](/examples/todomvc-declarative-mutations/src/todomvc/commands.cljs#L40).\n\nAlso, I declared [promesa](https://github.com/funcool/promesa) as a Mook\ndependency. This is intentional since it exposes a very nice API to work with\nasync logic. **In other words, async logic of Mook commands should be structured\nwith promesa.**  \nCheck this part of the [TodoMVC\nexample](https://github.com/lambdam/mook/blob/14ef9df029ddb8a72ff8b5fed5c0a318c9360fac/examples/todomvc-mook-wrapper/src/todomvc/components.cljs#L42).\n\n\n## Why \"Mook\"\n\nThe main tools used in Mook are Promises and Hooks.\n\nWe could craft names such as \"Pook\" or \"Prooks\"... but that doesn't sound very\ngood. And since promises and monads are conceptually very close, we can say that\nthe library is about MOnads and hoOKs: \"Mook\".\n\n\n## Examples\n\nI applied Mook to the TodoMVC project and included the sources of the examples\nin the repository, in the `examples` folder:\n\n- TodoMVC with [Mook own React wrapper and the commands with direct ref mutations](/examples/todomvc-direct-ref-mutations).\n- TodoMVC with [Mook own React wrapper and the commands with declarative mutations](/examples/todomvc-declarative-mutations).\n- TodoMVC with the [hicada library](/examples/todomvc-hicada).  \n  [Hicada](https://github.com/rauhs/hicada) is a hiccup compiler for ClojureScript.\n- [COMING SOON] TodoMVC with the helix library.\n\n\n## Todo\n\n* [ ] Unit tests\n* [ ] Server side rendering for the small wrapper\n\n\n## License\n\nCopyright © 2020 Damien RAGOUCY\n\nDistributed under the [MIT License](/license.txt)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flambdam%2Fmook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flambdam%2Fmook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flambdam%2Fmook/lists"}