[Legacy v0.1.0] Mook, a novel approach to frontend state management with ClojureScript and React
⚠ Note: this is the very first version of the Mook article. After getting many feedbacks with the this article, I introduced deep changes in the code, published a new version of the article and the library. ⚠
The default way to write UI applications in ClojureScript is to use React or its cousin React Native.
UI applications involve handling the following things:
- Displaying graphical components (~ how to describe them)
In this article we will use a very shallow wrapper around React described in a previous article and included in the mook library. - How to store and transform state (through user action or external action).
This is the topic of this article. - How to link state and UI.
React handles this point with the following "mantra": UI is a strict (and "dumb") application of the state:UI = f(state)
.
Mook is a young library that gives tools to store application state and to transform it.
After building ClojureScript applications (with React and React Native) for about 6 years, I bumped into various limitations. This library is an attent to circumvent them.
Throughout this article, we'll discover Mook ideas interactively thanks to Yehonathan Sharvit's wonderfull Klipse library. The ClojureScript code is compiled and executed directly in your browser so 1. you will see the result of the code next to it and 2. you can modify the code to explore its behavior.
Global state management
React handles only local state at the component level. But handling the global app state is another affair, and we have to turn toward other libraries (or manually crafting it around the ideas of Flux).
A traditional way to handle global state in a Javascript is the Redux approach. In the ClojureScript land, the reference is re-frame, which has similar concepts, with different names.
Here are the Redux important ideas:
- Isolate the application reference state into one hashmap structure.
- Expose an opiniated way to read this state by preventing the risk of unwanted mutations.
- Expose an opiniated way to transform this state.
re-frame, in contrast to Redux, doesn't suffer from unwanted mutation risks thanks to Clojure persistent data structures.
Let's explore some problems and limitations of those frameworks of ideas.
Limitation #1: state reading
It's funny to note on a Clojure blog post that one of the first notions introduced in Redux essential tutorial is immutability. Reading the state with useSelector
exposes the risk of mutating the reference state with methods such as array.sort(). This kind of risk doesn't exist in the Clojure land thanks to its persistent data structures.
Also on the Redux side, new values suffer from the fact that the comparison of objects in JS are based on references, not values. So every new value that is an object will fire a full tree comparison, as explained on this page (in the 4th paragraph).
Clojure doesn't suffer from that since it does structural comparison by default, treating all data as values.
Finally, re-frame forces one to read the state in a component with a subscription. It forces to split some intention between different namespaces. After a while, changing and/or cleaning those linked parts of the code can be tricky.
To summarize: Since Clojure data structures are immutable and use structural comparison, we could locally and directly read it in the component and use Clojure structural equality to fire re-renders.
Limitation #2: state writing
Redux takes a lot of care to prevent unwanted mutations. So it defines a clear semantics to handle precise state mutations: this is the notion of reducer. Here is the shape of a reducer:
function counterReducer(state, action) {
... // state tranformation here
return newState;
}
Clojure already has a clear interface for state transition: reset!
and swap!
functions on atoms. But re-frame bypasses this interface, works hard to hide it and exposes a signature similar to Redux:
(defn effect-handler [coeffects event]
(let [item-id (second event)
db (:db coeffects)
new-db (dissoc-in db [:items item-id])]
{:db new-db}))
In the name of the superiority of data over everything (data > functions > macros
in the intro page of re-frame), some clean and simple properties of functions (composition, closures) and already existing pieces of the standard library (atoms) aren't used directly. This mantra might bring unwanted complexity.
To summarize: Since Clojure already has a clear and clean way to handle state transition, why not using it directly instead of hiding it?
Limitation #3: async workflows
Let's talk only about re-frame.
This point is actually the one that pushed me away from re-frame: the existence of the re-frame-async-flow-fx library.
Having to redefine all async logic with pure data deeply is questionable when promises and core.async already exist.
Once again, the mantra of data > function > macro
seems to push away from the simplicity of some elegantly designed tools that already exist.
Complex async logic usually appears in the initialization phase of many applications. This library (and re-frame in general) forces to split an intention into multiple coupled event effects when a chain of promises can keep together async pieces of business logic.
To summarize: Why not using tools that already exist to handle async workflows?
Limitation #4: the single hashmap, in-memory database
Last but not least, the idea that putting all the reference state into one hashmap is actually a good idea.
Let's imagine that we are creating a startup and that as CTO, we have to create an UI that allows users to pin books that they liked and recommand them to other users. One of the user stories is that on the user's main public page there is the list of read books and every book has information of the title, the author and a list of categories it belongs to.
You set up the database, expose a CRUD API and start coding the UI with one big hashmap to store the reference state.
Following the user story, a natural way of storing the data and exposing it on a coupled API endpoint would be the following:
{:current-user-id 1234
:users [{:id 1234
:name "Dam"
:relations [2345 ...]
:books [{:id 9
:name "Foo"
:author "..."
:summary "..."
:categories [{:id 1
:name "Thriller"}
{:id 2
:name "Nordic"}]}
{:id 8
:name "Bar"
:author "..."
:summary "..."
:categories [{:id 3
:name "Horror"}
{:id 2
:name "Nordic"}]}]}
{:id 2345
:name "Chpill"
...}]}
After some time, the product manager asks for a new feature that would allow users to browse the books also by categories.
😤 No problem, I'm a tough programmer! Let's write a function that extracts this information from the big database hashmap.
(defn get-books-by-categories [db]
(some->> (:users db)
(some #(when (= (:id %) (:current-user-id db))
%))
:books
(reduce (fn [acc book]
(reduce (fn [acc category]
(if (contains? acc category)
(update acc category conj book)
(assoc acc category [book])))
acc
(:categories book)))
{})))
Yeah! I'm tough and Clojure is a blessing since I can put hashmaps as keys of hashmaps. The result would look like this:
{{:id 1
:name "Thriller"} [{:id 9
:name "Foo"
...}
...]
{:id 2
:name "Nordic"} [{:id 9
:name "Foo"
...}
{:id 8
:name "Bar"
...}
...]
...}
But... 🤔
Maybe, that is not such a good idea. When I come back a month later, I struggle to understand what this function does and what is the shape of the data flowing through it.
To summarize: The hashmap data structure might not be very good as an in memory database since it forces one to give a "purpose shape" to the data.
Let's meet Mook
Mook is the synthesis of about 6 years of crafting applications with ClojureScript. It came to life thanks to:
- React Hooks.
- Datascript, a "true" in-memory database with powerful query capabilities.
- Étienne Spillmaeker, aka @chpill, my Clojure buddy since the beginning of my Clojure adventure.
It tries to address the 4 pain points listed above.
Here are the strong Mook ideas:
- Use the db directly in the component with handlers.
- Use Clojure references directly (generally atoms) in state transitions (aka actions).
- Use promises to handle async workflows.
- Make actions composable.
- Enable the use of Datascript along with atoms to store state.
Let's see how we could circumvent the previous limitations and then merge it into a usable library. We'll continue with our book platform and implement it with Mook interactively in the page thanks to Klipse.
Step #1: the data(base(s))
One dominant idea of various architectures around React is to store the reference data in one place instead of spreading it all over the application.
Sadly we saw that this great idea leads to store the data into one big nested hashmap and that hashmaps aren't that good to store and extract information of "complex" data models.
Another great thing with Clojure is that it provides atoms which were from the beginning crafted with database-like interfaces.
Sadly again, a Datascript database and an atom do not share exactly the same interface for writing and listening (but do for reading, through the deref
mechanism).
This can be summarized like this:
Reading | Writing | Watching | |
---|---|---|---|
atom | deref |
reset! , swap! |
add-watch , remove-watch |
Datascript db | deref |
transact! |
listen! , unlisten! |
We'll see later that in the Mook case, having different writing interfaces is not important, but watching interfaces are very important for React re-renders on data change.
Furtunately, Clojure provides a clever tool for this case: the protocol mechanism. This will permit to unify watching interfaces between Clojure atoms and Datascript databases (and more structures later if required).
(defprotocol Watchable
(listen! [this key f])
(unlisten! [this key]))
Also the handler provided to add-watch
(atom) and listen!
(Datascript db) do not have the same signature. This will also be the occasion to unify this.
Mook already implements this protocol for Clojure atoms so there is nothing for them to do. But Mook doesn't define Datascript as a mandatory dependency (even though not using it would be a great loss). So if we decide to use it, we would have to manually implement this protocol in our project.
Note: the warnings are a Klipse integration problem, but the code works well.
(ns blog.core)
(require '[mook.core :as m])
(require '[datascript.core :as d])
(require 'datascript.db)
(extend-type datascript.db/DB
m/Watchable
(m/listen! [this key f]
(d/listen! this key (fn watch-changes [{:keys [db-after] :as _transaction-data}]
(f {::m/new-state db-after}))))
(m/unlisten! [this key]
(d/unlisten! this key)))
At this point, we are able to use an atom or a datascript db. But it could also be an atom AND a datascript db. It is widely admited that one place for the whole state is the way to go on frontend. But on the backend, many architectures have a "heavy" database (like PostgreSQL) for "reference" data and a more lightweight one (like Redis) for "volatile" information. The backend state is therefor split between specialized data stores.
Let's then introduce the notion of state stores. In the end, a state store is simply a structure that has the same reading, writing and watching behavior than a atom.
In the previous project that I built, the state was split this way:
- A Datascript db that held the reference data that came from Datomic and that was shared across various clients.
- A Clojure atom called the "local store" that was responsible to hold the app local state, typically the current user id.
- A Clojure atom called the "inert store" that would hold data that would never directly fire UI re-renders, typically the Firebase connection.
Step #2: reading the state(s)
Now that we know where to store our data, let's start building the UI and read the data from a component.
For the time being, Mook defines two hooks to read the data:
- One simple hook (
use-state-store
) that takes a state store name and a handler. The handler receives the dereferenced store as its first and only parameter. There are only two ways for this hook to fire a re-render: 1. the result of the handler changes (the handler might close over changing values) 2. the state store changes and the result of the previous known handler changes. - A more evolved one (
use-param-state-store
), similar to React behaviour with componentkey
attibute, where the developer controls the data that will provoke a new comparison. This hook was crafted to address the fact that complex queries in Datascript might be slow, and we don't want it to replay on every functional component call. Also this hook fires a re-render when the "key" value changes or that the result of a new state of the store changes.
We have to note that old and new values are compared with Clojure =
function. This function works at the data level (structural comparison), not the reference level so JS objects might not play well with Mook hooks (once again, as described in the Redux documentation).
Let's build our interface with Mook. We will first simply display the current user's book list with the information of title, author and categories for every book.
Let's go back to the point where the product owner asks us to browse books by categories. Instead of jumping into the existing structure of the data, we could have taken a step back and considered the relations between our entities (the following diagram is a kind of SQL entity-relation diagram adapted to Datomic/Datascript semantics)
We will use Datascript to store and query this data instead of a hashmap.
When initializing a Datascript db, we can give it a schema so that it can normalize our data (~ understand the relations between the entities).
(def db-schema
{:user/books {:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many}
:user/favorite-books {:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many}
:book/categories {:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many}})
And here is some data for our database.
(def db-data
(let [id* (atom 0)
get-new-id! #(swap! id* inc)
cat1 {:db/id (get-new-id!)
:category/name "Thriller"}
cat2 {:db/id (get-new-id!)
:category/name "History"}
cat3 {:db/id (get-new-id!)
:category/name "SciFi"}
cat4 {:db/id (get-new-id!)
:category/name "Teenager"}
cat5 {:db/id (get-new-id!)
:category/name "Architecture"}
cat6 {:db/id (get-new-id!)
:category/name "Biography"}
cat7 {:db/id (get-new-id!)
:category/name "Geek"}
book01 {:db/id (get-new-id!)
:book/title "Cochlearius cochlearius"
:book/author "Chryste Metherell"
:book/categories [cat1 cat2]}
book02 {:db/id (get-new-id!)
:book/title "Meleagris gallopavo"
:book/author "Harold Frandsen"
:book/categories [cat3 cat4 cat7]}
book03 {:db/id (get-new-id!)
:book/title "Trachyphonus vaillantii"
:book/author "Nickola Joderli"
:book/categories [cat6 cat2]}
book04 {:db/id (get-new-id!)
:book/title "Alopochen aegyptiacus"
:book/author "Hersh Eliasen"
:book/categories [cat5 cat6]}
book05 {:db/id (get-new-id!)
:book/title "Loxodonta africana"
:book/author "Ciel Kabos"
:book/categories [cat1 cat7 cat6]}]
[{:db/id (get-new-id!)
:user/firstname "Virgil"
:user/lastname "Peron"
:user/books [book01 book05]
:user/favorite-books [book01]}
{:db/id (get-new-id!)
:user/firstname "Lalo"
:user/lastname "Dumont"
:user/books [book01 book02 book03 book04 book05]
:user/favorite-books [book03 book04]}]))
Then let's initialize our database:
Note: In a real application, the data would come from an initialization phase and would be stored through an action. We transact it direclty here for the convenience of the article.
(defonce app-db*
(d/create-conn db-schema))
(d/transact! app-db* db-data)
Let's build our app with two state stores:
- A Datascript database that will hold the reference data (basically, the data that would flow troughout the whole architecture, front and back)
- A Clojure atom that we will use as a lightweight key-value store (essentially for the current user id)
We will register those state stores in Mook so that we can use them in our reading hooks (use-state-store
and use-param-state-store
).
(m/register-store! ::app-db* app-db*)
(defonce local-store*
(let [id (d/q '[:find ?e .
:where
[?e :user/firstname "Lalo"]]
@app-db*)]
(atom {::current-user-id id})))
(m/register-store! ::local-store* local-store*)
And now, let's build the page asked by the product owner: the list of user books grouped by category.
We will use the tiny React wrapper included in mook that relies on cljs-bean. This wrapper is an optimized version of the one described in my previous article.
But you could use any wrapper that uses a modern version of React that exposes the hooks API.
Since we are developping an application for the browser, we can use the handy mook macro that will define all official HTML tags in a custom namespace.
(ns blog.tags)
(require 'mook.react)
(refer-clojure :exclude '[map meta time])
(mook.react/def-html-elems!)
And now the page:
(ns blog.core)
(require '[mook.core :as m])
(require '[mook.react :as mr])
(require '[blog.tags :as t])
(require '[cljs-bean.core :as b])
(require '[promesa.core :as p])
(require '[datascript.core :as d])
(defn book-by-category [props']
(let [props (b/->clj props')
[cat-id book-ids :as cat->book] (:cat->book props)
favorite-book-ids-set (-> props :favorite-book-ids set)
category (m/use-state-store ::app-db* #(d/pull % '[*] cat-id))
books (m/use-state-store ::app-db* #(d/pull-many % '[*] book-ids))]
(t/div {:className "card mb-3"}
(t/div {:className "card-body"}
(t/h5 {:className "card-title"}
(:category/name category))
(apply mr/fragment
(->> (for [book books]
(mr/fragment
(t/small {:className "font-italic"}
(:book/title book))
" - "
(t/small (:book/author book))))
(interpose (t/br))))))))
(defn user-books-by-category []
(let [user-id (m/use-state-store ::local-store* ::current-user-id)
user (m/use-param-state-store ::app-db*
user-id
#(d/pull % [:user/firstname :user/lastname] user-id))
favorite-book-ids (m/use-state-store ::app-db*
#(some->> (d/pull % [:user/favorite-books] user-id)
:user/favorite-books
(map :db/id)))
cat->books (m/use-param-state-store
{::m/store-key ::app-db*
::m/params [user-id favorite-book-ids]
::m/handler (fn [db]
(->> (d/q '[:find ?cat-id ?book-id
:in $ ?user-id
:where
[?user-id :user/books ?book-id]
[?book-id :book/categories ?cat-id]]
db user-id)
(reduce (fn [acc [cat-id book-id]]
(update acc cat-id #(-> (or % [])
(conj book-id))))
{})))})]
(t/div {:className "card"}
(t/div {:className "card-body"}
(t/h4 {:className "card-title"}
(:user/firstname user) " "
(:user/lastname user)
"'s books by category")
(for [cat->book cat->books]
(mr/create-element book-by-category {:key (str (print-str cat->book) favorite-book-ids)
:favorite-book-ids favorite-book-ids
:cat->book cat->book}))))))
(js/ReactDOM.render
(m/mook-state-store-container
(mr/create-element user-books-by-category))
(js/document.getElementById "mook-block-1"))
Loading ...
There are various things to note here:
1 - Mook hooks have two arities: the unary one (use-state-store
spec and use-param-state-store
spec) that accepts a map with all parameters explicitly given. In a way, this arity acts like labelled arguments in other languages (like OCaml for example). Respectively the binary and ternary arities with positional arguments act like shorthand versions of the unary function call.
(require '[mook.core :as m])
;; Arity 1
(use-state-store {::m/store-key ::local-store*
::m/handler (fn [store]
(::current-user-id store))})
;; Arity 2 (shorthand)
(use-state-store ::db* ::current-user-id)
;; ---
;; Arity 1
(use-param-state-store {::m/store-key ::db*
::m/params [current-user-id book-ids]
::m/handler (fn [db] ...)})
;; Arity 3 (shorthand)
(use-param-state-store ::db*
[current-user-id book-ids]
(fn [db] ...))
2 - The Datalog query is infinitely more expressive than the code written in the "Limitation #4" section. Let's write it again:
[:find ?cat-id ?book-id
:in $ ?user-id
:where
[?user-id :user/books ?book-id]
[?book-id :book/categories ?cat-id]]
In 5 lines, we read the whole intention: get all the category and book id pairs that belong to a given user. In the other case, you have to know the structure of the data to understand what the code does.
3 - The state reading is local. When we change or delete the component for any business reason, there is little chance to have dead code hanging around the project.
Step #3: changing the state(s)
Finally, let's simulate a whole webapp with a new requirement from the product owner. Those are the demands:
- We now have two pages. One for the list of books ordered by categories AND another one with the list of the user's books with their categories. We will simulate the page navigation with tabs and a field in the lightweight key-value state store (the Clojure atom).
- Users can pin their favorite books on the book list page. But there is more. The UX designers come with precise instructions on interactions. When the user clicks on a "pin" button, no other action can happen while the current one isn't finished. Also, to indicate the book being updated, a specific icon must appear only on the concerned book.
Let's go!
(require '[clojure.spec.alpha :as s])
(s/def ::current-route #{:by-book :by-category})
(s/def ::in-progress? boolean?)
;; For the article purpose, we update the state store directly
(swap! local-store* merge {::current-route :by-book
::in-progress? false})
;; ---
;; We define our first command: mook semantics for actions
(defn set-route [{::keys [local-store*] :as data}]
(swap! local-store* merge (select-keys data [::current-route]))
(p/resolved (dissoc data ::current-route)))
;; Note that we can spec it! It is a regular function.
;; It is commented for a Klipse integration problem
#_(s/fdef set-route
:args (s/cat :data (s/keys :req [::local-store* ::current-route]))
:ret p/promise?)
;; Finally we register it so that it will receive the stores in its parameters
(m/register-command! ::set-route set-route)
;; ---
(s/def :db/id integer?)
(s/def ::book-id :db/id)
(s/def ::pending-favorite-book-id :db/id)
;; Our second command with async steps
(defn switch-favorite-book [{::keys [app-db* local-store* book-id]
:as data}]
(p/chain
(do (swap! local-store* assoc ::in-progress? true)
;; Simulate network call
(p/delay 1500 data))
#(let [user-id (::current-user-id @local-store*)
favorite? (-> (d/q '[:find [?book-id ...]
:in $ ?user-id
:where [?user-id :user/favorite-books ?book-id]]
@app-db* user-id)
set
(contains? book-id))]
(d/transact! app-db* [[(if favorite? :db/retract :db/add) user-id :user/favorite-books book-id]])
(swap! local-store* assoc ::in-progress? false)
(dissoc % ::book-id))))
#_(s/fdef switch-favorite-book
:args (s/cat :data (s/keys :req [::app-db* ::local-store* ::book-id]))
:ret p/promise?)
(m/register-command! ::switch-favorite-book switch-favorite-book)
;; ---
;; The SVG code of those icons is in a hidden code block
(declare sync-icon)
(declare plus-icon)
(declare star-icon)
Note: you can click on the tabs and on book card icons.
(defn book-detail [props']
(let [props (b/->clj props')
book-id (:book-id props)
favorite-book-ids-set (-> props :favorite-book-ids set)
in-progress? (m/use-state-store ::local-store* ::in-progress?)
[updating? set-updating!] (mr/use-state false)
favorite? (contains? favorite-book-ids-set book-id)
book (m/use-param-state-store
::app-db*
[book-id favorite-book-ids-set]
(fn [db]
(d/pull db '[* {:book/categories [*]}] book-id)))]
(t/div {:className "card mb-3"}
(t/div {:className "card-body"}
(t/h5 {:className "card-title font-italic"}
(:book/title book)
" "
(t/a {:href "#"
:className (mr/classes {"btn" true
"btn-small" true
"float-right" true
"btn-success" favorite?
"btn-outline-secondary" (not favorite?)
"disabled" in-progress?})
:onClick (fn switch-book-status [e]
(.preventDefault e)
(when (not in-progress?)
(set-updating! true)
(p/chain
(m/send-command>> {::m/type ::switch-favorite-book
::book-id book-id})
#(set-updating! false))))}
(cond
updating? (sync-icon)
favorite? (star-icon)
:else (plus-icon))))
(t/p {:className "card-text"}
(:book/author book))
(apply mr/fragment
(->> (for [category (:book/categories book)]
(t/small (:category/name category)))
(interpose (t/br))))))))
(defn user-books []
(let [user-id (m/use-state-store ::local-store* ::current-user-id)
user (m/use-param-state-store
::app-db*
user-id
(fn [db]
(d/pull db [:user/firstname :user/lastname] user-id)))
favorite-book-ids (m/use-state-store
::app-db*
(fn [db]
(d/q '[:find [?book-id ...]
:in $ ?user-id
:where [?user-id :user/favorite-books ?book-id]]
db user-id)))
book-ids (m/use-param-state-store
::app-db*
[user-id favorite-book-ids]
(fn [db]
(d/q '[:find [?book-id ...]
:in $ ?user-id
:where [?user-id :user/books ?book-id]]
db user-id)))]
(t/div {:className "card"}
(t/div {:className "card-body"}
(t/h4 {:className "card-title"}
(:user/firstname user) " "
(:user/lastname user)
"'s books")
(apply mr/fragment
(for [book-id book-ids]
(mr/create-element book-detail {:favorite-book-ids favorite-book-ids
:book-id book-id})))))))
(defn webapp-root []
(let [current-route (m/use-state-store ::local-store* ::current-route)]
(t/div {:className "p-2"}
(t/ul {:className "nav nav-tabs"}
(t/li {:className "nav-item"}
(let [current? (= current-route :by-book)]
(t/a {:className (mr/classes {"nav-link" true
"active" current?})
:href "#"
:onClick (fn [e]
(.preventDefault e)
(when (not current?)
(m/send-command>> {::m/type ::set-route
::current-route :by-book})))}
"By book")))
(t/li {:className "nav-item"}
(let [current? (= current-route :by-category)]
(t/a {:className (mr/classes {"nav-link" true
"active" (= current-route :by-category)})
:href "#"
:onClick (fn [e]
(.preventDefault e)
(when (not current?)
(m/send-command>> {::m/type ::set-route
::current-route :by-category})))}
"By category"))))
(mr/create-element (case current-route
:by-book user-books
:by-category user-books-by-category
(fn [] (t/p "Unknown page")))))))
(js/ReactDOM.render
(m/mook-state-store-container
(mr/create-element webapp-root))
(js/document.getElementById "mook-block-2"))
Loading ...
Once again, there many things to note in the code above:
- Traditionally, mutating the global state is done through "actions". I chose another semantics after a discussion with my friend @chpill: "commands". The semantics is taken from the event sourcing architecture that distinguishes "facts", things that happened for sure, and "commands", sending the intention of a transformation. But this command can fail for many reasons. A command in mook is a function that takes a map and returns a promise that resolves to a map. The promise expresses the fact that the future result of a command can be a success or a failure. Also the promise has the useful property to be chainable.
- As an example of the usefulness of promise chainability, the
switch-book-status
click handler coordinates local and global effects: it first switches the icon of the concerned component (declared with auseState
hook), sends the global command and waits for it to finish to remove the "work in progress" icon. That I know, coordinating local and global effects isn't easily feasible with existing libraries. Once again, thinking about re-frame, enforcing data everywhere might not be such a good idea since achieving this with pure data isn't possible and has to be encapsulated with a library such as re-frame-async-flow-fx. Here we use two very useful properties of functions (in the programming sense): composability and closing over values (closures). - [Update] I declared promesa as a Mook dependency. This is intentional since it exposes a very nice API to work with async logic. In other words, async logic of Mook commands should be structured with promesa.
- We use references directly in commands. Once again, we do so since Clojure references have sane read and write interfaces on top of immutable data structures. It is impossible to inadvertently mutate the reference state of our application.
A final word: why "Mook"? Because this library is essentially made out of promises and hooks. Mixing those words would give "Pook" or "Prooks". But that doesn't sound very good. Since promises and monads are conceptually very similar, we can think of monads and hooks: "Mook"!
And also, monads make you look smart 😏.
https://github.com/lambdam/mook/
Thanks
I want to thank particularly two persons:
- Étienne Spillmaeker (@chpill) for his patience and his clever feedbacks on Mook maturation.
- Yehonathan Sharvit (@viebel) for his time taken to make this interactive article with Klipse possible.
I want to thank too Nikita Prokopov (@tonsky) for developing Datascript, which is IMO a key tool for reducing the complexity of frontend applications.
[update] Many thanks to @walterl for carefully reading and correcting my English mistakes in the article.
I'd like also to thank the Paris Clojure Meetup community for being such a vibrant group of interesting and clever persons.
And finally Rich Hickey and the Clojure core team for... creating Clojure.
Damien RAGOUCY - Summer 2020