Mook, a novel approach to state management with ClojureScript and React

0

The default procedure to write UI functions in ClojureScript is to make employ of React or its cousin React Native.

UI functions comprise handling the next issues:

  1. Showing graphical parts (~ easy techniques to listing them)

    In this article we can employ a in point of fact shallow wrapper round React described in a old article and integrated in the mook library.
  2. Guidelines on how to store and became voice (by client lunge or external lunge).

    Right here’s the subject of this article.
  3. Guidelines on how to link voice and UI.

    React handles this point with the next “mantra”: UI is a strict (and “dull”) application of the voice: UI = f(voice).

Mook is a younger library that provides tools to store application voice and to remodel it.

After constructing ClojureScript functions (with React and React Native) for about 6 years, I bumped into diversified boundaries. This library is an attent to avoid them.

All over this article, we will notion Mook suggestions interactively thanks to Yehonathan Sharvit’s wonderfull Klipse library. The ClojureScript code is compiled and completed straight in your browser so 1. you are going to see the outcomes of the code next to it and a pair of. that it’s probably you’ll maybe presumably also alter the code to explore its habits.

World voice administration

React treats top about native voice administration. But handling the global app voice is yet any other affair, and we must all the time flip towards other libraries (or manually crafting all of it over the suggestions of Flux)

A mature procedure to take care of global voice in a Javascript is the Redux procedure. In the ClojureScript land, the reference is re-frame, which has equal concepts, with varied names.

Listed below are the Redux crucial suggestions:

  • Isolate the application reference voice into one hashmap structure.
  • Present an opiniated procedure to be taught this voice by preventing the possibility of unwanted mutations.
  • Present an opiniated procedure to remodel this voice.

re-frame, in distinction to Redux, would now not endure from unwanted mutation risks thanks to Clojure continual data structures.

Let’s explore some complications and boundaries of those frameworks of suggestions.

Limitation #1: voice studying

Or no longer it’s funny to present on a Clojure weblog publish that one in every of the first notions launched in Redux valuable tutorial is immutability Reading the voice with useSelector exposes the possibility of mutating the reference voice with techniques similar to array.kind(). This extra or less possibility would now not exist in the Clojure land thanks to its continual data structures.

Also on the Redux facet, contemporary values endure from the truth that the comparison of objects in JS are in response to references, no longer values. So every contemporary worth that’s an object will fire a fleshy tree comparison, as explained on this page (in the 4th paragraph).

Clojure would now not endure from that since it does structural comparison by default, treating all data as values.

At last, re-frame forces to be taught the voice in a ingredient with a subscription. It forces to atomize up some procedure between varied namespaces. After a while, altering and/or cleaning those linked parts of the code can even be refined.

To summarize: Since Clojure data structures are immutable and employ structural comparison, we would in the community and straight be taught it in the ingredient and employ Clojure structural equality to fireside re-renders.

Limitation #2: voice writing

Redux takes a great deal of care to stop unwanted mutations. So it defines a clear semantics to take care of proper voice mutations: here’s the figuring out of reducer. Right here is the form of a reducer:

feature counterReducer(voice, lunge) {
  ... // voice tranformation here
  return newState;
}

Clojure already has a clear interface for voice transition: reset! and swap! functions on atoms. But re-frame overpasses this interface, works no longer easy to mask it and exposes a signature corresponding to Redux:

(defn lift out-handler [coeffects event]
 (let [item-id  (second event)
       db       (:db coeffects)
       new-db   (dissoc-in db [:items item-id])]
   {:db contemporary-db}))

In the name of the superiority of data over every thing (data > functions > macros in the intro page of re-frame), some super and straightforward properties of functions (composition, closures) and already present items of the peculiar library (atoms) are now not feeble straight. This mantra might maybe lift unwanted complexity.

To summarize: Since Clojure already has a clear and super procedure to take care of voice transition, why no longer using it straight reasonably than hiding it?

Limitation #3: async workflows

Let’s talk top about re-frame.

This point is de facto the one which pushed me a ways flung from re-frame: the existence of the re-frame-async-circulate-fx library.

Why and when a library that redefines your entire async good judgment with pure data is a factual belief, when promises and core.async already exist?

As soon as extra, the mantra of data > feature > macro looks to push a ways flung from the simplicity of some elegantly designed tools that already exist.

To summarize: Why no longer using tools that already exist to take care of async workflows?

Limitation #4: the top hashmap, in-memory database

Last however no longer least, the belief that striking your entire reference voice into one hashmap is de facto a factual belief.

We might maybe presumably hiss that we are increasing a startup and that as CTO, we must all the time construct an UI that permits customers to pin books that they cherished and recommand them to other customers. One amongst the client reviews is that on the client’s valuable public page there might be the list of be taught books and every e book has data of the title, the author and a list of categories it belongs to.

You attach up the database, snarl a CRUD API and initiate coding the UI with one reliable hashmap to store the reference voice.

Following the client narrative, a natural map of storing the data and exposing it on a coupled API endpoint might maybe presumably be the next:

{:most modern-client-identity 1234
 :customers [{:id 1234
          :name "Dam"
          :relations [2345 ...]
          :books [{:id 9
                   :name "Foo"
                   :author "..."
                   :summary "..."
                   :categories [{:id 1
                                 :name "Thriller"}
                                {:id 2
                                 :name "Nordic"}]}
                  {:identity 8
                   :name "Bar"
                   :author "..."
                   :summary "..."
                   :categories [{:id 3
                                 :name "Horror"}
                                {:id 2
                                 :name "Nordic"}]}]}
         {:identity 2345
          :name "Chpill"
          ...}]}

After some time, the product supervisor asks for a contemporary feature that might maybe presumably allow customers to browse the books also by categories.

😤 No subject, I’m a no longer easy programmer! Let’s write a feature that extracts this data from the reliable database hashmap.

(defn earn-books-by-categories [db]
  (some->> (:customers db)
           (some #(when (= (:identity %) (:most modern-client-identity db))
                    %))
           :books
           (crop (fn [acc book]
                     (crop (fn [acc category]
                               (if (accommodates? acc category)
                                 (update acc category conj e book)
                                 (assoc acc category [book])))
                             acc
                             (:categories e book)))
                   {})))

Yeah! I’m no longer easy and Clojure is a blessing since I can attach hashmaps as keys of hashmaps. The end result would leer admire this:

{{:identity 1
  :name "Thriller"} [{:id 9
                      :name "Foo"
                      ...}
                     ...]

 {:identity 2
  :name "Nordic"} [{:id 9
                    :name "Foo"
                    ...}
                   {:id 8
                    :name "Bar"
                    ...}
                   ...]

 ...}

But… 🤔

Maybe, that’s no longer one of these factual belief. As soon as I come support a month later, I war to worth what this selection does and what is the form of the data flowing by it.

To summarize: The hashmap data structure might maybe no longer be very factual as an in memory database since it forces to give a “objective form” to the data

Let’s meet Mook

Mook is the synthesis of about 6 years of crafting functions with ClojureScript. It came to lifestyles thanks to:

  1. React Hooks.
  2. Datascript, a “true” in-memory database with extremely efficient inquire of capabilities.
  3. Étienne Spillmaeker, aka @chpill, my Clojure buddy since the starting of my Clojure lope.

It tries to address the 4 anxiety aspects listed above.

Listed below are the sturdy Mook suggestions:

  • Use the db straight in the ingredient with handlers.
  • Use straight Clojure references (on occasion atoms) in voice transitions (aka actions).
  • Use promises to take care of async workflows.
  • Originate actions composable.
  • Enable using Datascript collectively with atoms to store voice.

Let’s occupy a occupy a study how we would circumvent the old boundaries after which merge it valid into a usable library. We will proceed with our e book platform and implement it with Mook interactively in the page thanks to Klipse

Step #1: the data(horrible(s))

One dominant belief of various architectures round React is to store the reference data in one dwelling reasonably than spreading all of it over the application.

Sadly we noticed that this giant belief leads to store the data into one reliable nested hashmap and that hashmaps are now not that factual to store and extract data of “advanced” data devices.

One more giant thing with Clojure is that it provides atoms that occupy been from the starting crafted with database-admire interfaces.

Sadly all over again, a Datascript database and an atom sign no longer half the right kind same interface for writing and listening (however sign for studying with by the deref mechanism).

That is liable to be summarized admire this:

Reading Writing Staring at
atom deref reset!, swap! add-see, take-see
Datascript db deref transact! listen!, unlisten!

We will see later that in the Mook case, having varied writing interfaces isn’t any longer crucial, however staring at interfaces are crucial for React re-renders on data commerce.

Furtunately, Clojure provides a colorful instrument for this case: the protocol mechanism. This might maybe enable to unify staring at interfaces between Clojure atoms and Datascript databases (and extra structures later if required).


(defprotocol Watchable
  (listen! [this key f])
  (unlisten! [this key]))
 

Also the handler supplied to add-see (atom) and listen! (Datascript db) sign no longer occupy the same signature. This also shall be the occasion to unify this.

Mook already implements this protocol for Clojure atoms so there might be nothing to sign for them. But Mook would now not account for Datascript as a valuable dependency (even supposing no longer using it’d be a giant loss). So if we issue to make employ of it, we’d need to manually implement this protocol in our mission.

Point to: the warnings are a Klipse integration subject, however the code works well.


(ns weblog.core)

(require '[mook.core :as m])
(require '[datascript.core :as d])
(require 'datascript.db)

(extend-form datascript.db/DB
  m/Watchable
  (m/listen! [this key f]
    (d/listen! this key (fn see-changes [{:keys [db-after] :as _transaction-data}]
                          (f {::m/contemporary-voice db-after}))))
  (m/unlisten! [this key]
    (d/unlisten! this key)))
 

At this point, we are in a area to make employ of an atom or a datascript db. But it goes to moreover be an atom AND a datascript db. It is broadly admited that one dwelling for the whole voice is be taught the procedure to transfer on frontend. But on the backend, many architectures occupy a “heavy” database (admire PostgreSQL) for “reference” data and a extra lightweight one (admire Redis) for “volatile” data. The backend voice is therefor splitted between undoubtedly just correct data stores.

Let’s then introduce the figuring out of voice stores. In the live, a voice store is merely a structure that has the same studying, writing and staring at habits than a atom.

In the old mission that I constructed, the voice used to be splitted this map:

  • A Datascript db that held the reference data that came from Datomic and that used to be shared all over diversified purchasers.
  • A Clojure atom called the “native store” that used to be responsible to retain the app native voice, on occasion basically the most modern client identity.
  • A Clojure atom called the “inert store” that might maybe presumably retain data that might maybe presumably never straight fire UI re-renders, on occasion the Firebase connection.

Step #2: studying the voice(s)

Now that we know where to store ou data, let’s initiate constructing the UI and be taught the data from a ingredient.

For the time being, Mook defines two hooks to be taught the data:

  • One easy hook (employ-voice-store) that takes a voice store name and a handler. The handler receives the dereferenced store as its first and top parameter. There are top two techniques for this hook to fireside a re-render: 1. the outcomes of the handler changes (the handler might maybe terminate over altering values) 2. the voice store changes and the outcomes of the old acknowledged handler changes.
  • A extra developed one (employ-param-voice-store), corresponding to React behaviour with ingredient key attibute, where the developper controls the data that will provoque a contemporary comparison. This hook used to be crafted to address the truth that advanced queries in Datascript also can very well be sluggish, and we sign no longer desire it to replay on every purposeful ingredient name. Also this hook fires a re-render when the “key” worth changes or that the outcomes of a contemporary voice of the store changes.

Now we occupy got to present that mature and contemporary values are compared with Clojure = feature. This selection works at the data level (structural comparison), no longer the reference level so JS objects might maybe no longer play well with Mook hooks (as soon as all over again, as described in the Redux documentation).

Let’s originate our interface with Mook. We will first merely demonstrate basically the most modern client’s e book list with the data of title, author and categories for every e book.

Let’s return to the point where the product owner asks us to browse books by categories. Rather then jumping in the present structure of the data, we would occupy taken a step support and get into consideration the family between our entities (the next design is a extra or less SQL entity-relation design tailored to Datomic/Datascript semantics)

er diagram

We will employ Datascript to store and inquire of this data reasonably than a hashmap.

When initializing a Datascript db, we might give it a schema so that it might normalize our data (~ realize the family between the entities).


(def db-schema
  {:client/books {:db/valueType :db.form/ref
                :db/cardinality :db.cardinality/many}
   :client/favorite-books {:db/valueType :db.form/ref
                         :db/cardinality :db.cardinality/many}
   :e book/categories {:db/valueType :db.form/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! idinc)
        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]}
        e book02 {:db/identity (earn-contemporary-identity!)
                :e book/title "Meleagris gallopavo"
                :e book/author "Harold Frandsen"
                :e book/categories [cat3 cat4 cat7]}
        e book03 {:db/identity (earn-contemporary-identity!)
                :e book/title "Trachyphonus vaillantii"
                :e book/author "Nickola Joderli"
                :e book/categories [cat6 cat2]}
        e book04 {:db/identity (earn-contemporary-identity!)
                :e book/title "Alopochen aegyptiacus"
                :e book/author "Hersh Eliasen"
                :e book/categories [cat5 cat6]}
        e book05 {:db/identity (earn-contemporary-identity!)
                :e book/title "Loxodonta africana"
                :e book/author "Ciel Kabos"
                :e book/categories [cat1 cat7 cat6]}]
    [{:db/id (get-new-id!)
      :user/firstname "Virgil"
      :user/lastname "Peron"
      :user/books [book01 book05]
      :client/favorite-books [book01]}
     {:db/identity (earn-contemporary-identity!)
      :client/firstname "Lalo"
      :client/lastname "Dumont"
      :client/books [book01 book02 book03 book04 book05]
      :client/favorite-books [book03 book04]}]))
 

Then let’s initialize our database:

Point to: In a proper application, the data would come from an initialization fragment and might maybe presumably be kept by an lunge. We transact it direclty here for the ease of the article.


(defonce app-db(d/construct-conn db-schema))

(d/transact! app-dbdb-data)
 

Let’s originate our app with two voice stores:

  • A datascript database that will retain the reference data (on occasion, the data that might maybe presumably circulate troughout your entire structure, front and support)
  • A Clojure atom that we are going to employ as a lightweight key-worth store (truly for basically the most modern client identity)

We will register those voice stores in Mook so that we can employ them in our studying hooks (employ-voice-store and employ-param-voice-store).


(m/register-store! ::app-dbapp-db*)

(defonce native-store(let [id (d/q '[:find ?e .
                  :where
                  [?e :user/firstname "Lalo"]]
                @app-db*)]
    (atom {::most modern-client-identity identity})))

(m/register-store! ::native-storenative-store*)
 

And now, let’s originate the page requested by the product owner: the list of client books grouped by category.

We will employ the puny React wrapper integrated in mook that relies on cljs-bean. This wrapper is an optimized version of the one described in my old article.

But that it’s probably you’ll maybe presumably presumably employ any wrapper that uses a accepted version of React that exposes the hooks API.

Since we are developping an application for the browser, we can employ the to hand mook macro that will account for all official HTML tags in a custom namespace.


(ns weblog.tags)

(require 'mook.react)
(refer-clojure :exclude '[map meta time])

(mook.react/def-html-elems!)
 

And now the page:

Code

(ns weblog.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 e book-by-category [props']
  (let [props (b/->clj props')
        [cat-id book-ids :as cat->book] (:cat->e book props)
        favorite-e book-ids-attach (-> props :favorite-e book-ids attach)
        category (m/employ-voice-store ::app-db#(d/pull % '[*] cat-identity))
        books (m/employ-voice-store ::app-db#(d/pull-many % '[*] e book-ids))]
    (t/div {:className "card mb-3"}
           (t/div {:className "card-body"}
                  (t/h5 {:className "card-title"}
                        (:category/name category))
                  (observe mr/fragment
                         (->> (for [book books]
                                (mr/fragment
                                  (t/puny {:className "font-italic"}
                                           (:e book/title e book))
                                  " - "
                                  (t/puny (:e book/author e book))))
                              (interpose (t/br))))))))
 
(defn client-books-by-category []
  (let [user-id (m/use-state-store ::local-store::current-user-id)
        user (m/use-param-state-store ::app-dbuser-id
                                      #(d/pull % [:user/firstname :user/lastname] client-identity))
        favorite-e book-ids (m/employ-voice-store ::app-db#(some->> (d/pull % [:user/favorite-books] client-identity)
                                                       :client/favorite-books
                                                       (blueprint :db/identity)))
        cat->books (m/employ-param-voice-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 client-identity)
                                         (crop (fn [acc [cat-id book-id]]
                                                   (update acc cat-identity #(-> (or % [])
                                                                           (conj e book-identity))))
                                                 {})))})]
    (t/div {:className "card"}
           (t/div {:className "card-body"}
                  (t/h4 {:className "card-title"}
                        (:client/firstname client) " "
                        (:client/lastname client)
                        "'s books by category")
                  (for [cat->book cat->books]
                    (mr/construct-voice e book-by-category {:key (str (print-str cat->e book) favorite-e book-ids)
                                                                 :favorite-e book-ids favorite-e book-ids
                                                                 :cat->e book cat->e book}))))))
 
(js/ReactDOM.render
  (m/mook-voice-store-container
    (mr/construct-voice client-books-by-category))
  (js/anecdote.getElementById "mook-block-1"))

There are many issues to present here:

1 – Mook hooks occupy two arities: the unary one (employ-voice-store spec and employ-param-voice-store spec) that accepts a blueprint with all parameters explicitly given. In a procedure, this arity acts admire labelled arguments in other languages (admire OCaml as an instance). Respectively the binary and ternary arities with positional arguments act admire shorthand variations of the feature name.

(require '[mook.core :as m])

;; Arity 1
(employ-voice-store {::m/store-key ::native-store::m/handler (fn [store]
                                (::most modern-client-identity store))})

;; Arity 2 (shorthand)
(employ-voice-store ::db::most modern-client-identity)

;; ---

;; Arity 1
(employ-param-voice-store {::m/store-key ::db::m/params [current-user-id book-ids]
                        ::m/handler (fn [db] ...)})

;; Arity 3 (shorthand)
(employ-param-voice-store ::db[current-user-id book-ids]
                       (fn [db] ...))

2 – The Datalog inquire of is infinitely extra expressive than the code written in the “Limitation #4” fragment. Let’s write all of it over 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 be taught the whole procedure: earn your entire category and e book identity pairs that belong to a given client. In the other case, you must know the structure of the data to worth what the code does.

3 – The voice studying is native. After we commerce or delete the ingredient for any industry reason, there might be puny chance to occupy listless code inserting all over the mission.

Step #3: altering the voice(s)

At last, let’s simulate a full webapp with a contemporary attach a query to of the product owner. These are the demands:

  • We occupy two pages. One for the list of books ordered by categories AND yet any other one with the list of the client’s books with their categories. We will simulate the page navigation with tabs and a area in the lightweight key-worth voice store (the Clojure atom).
  • Customers can pin their favorite books on the e book list page. But there might be extra. The UX designers include proper directions on interactions. When the client clicks on a “pin” button, no other lunge can happen while basically the most modern one isn’t any longer completed. Also, to illustrate the e book being up thus a ways, a selected icon need to seem top on the concerned e book.

Let’s streak!


(require '[clojure.spec.alpha :as s])

(s/def ::most modern-route #{:by-e book :by-category})
(s/def ::in-development? boolean?)

;; For the article objective, we update the voice store straight
(swap! native-storemerge {::most modern-route :by-e book
                           ::in-development? faux})

;; ---

;; We account for our first snarl: mook semantics for actions
(defn attach-route [{::keys [local-store*] :as data}]
  (swap! native-storemerge (capture out-keys data [::current-route]))
  (p/resolved (dissoc data ::most modern-route)))

;; Point to that we can spec it! It is a typical feature.
;; It is commented for a Klipse integration subject
#_(s/fdef attach-route
  :args (s/cat :data (s/keys :req [::local-store::current-route]))
  :ret p/promise?)

;; At last we register it so that it would receive the stores in its parameters
(m/register-snarl! ::attach-route attach-route)

;; ---

(s/def :db/identity integer?)
(s/def ::e book-identity :db/identity)
(s/def ::pending-favorite-e book-identity :db/identity)

;; Our second snarl with async steps
(defn swap-favorite-e book [{::keys [app-dblocal-storebook-id]
                             :as data}]
  (p/chain
    (sign (swap! native-storeassoc ::in-development? true)
        ;; Simulate network name
        (p/extend 1500 data))
    #(let [user-id (::current-user-id @local-store*)
           favorite? (-> (d/q '[:find [?book-id ...]
                                :in $ ?client-identity
                                :where [?user-id :user/favorite-books ?book-id]]
                              @app-dbclient-identity)
                         attach
                         (accommodates? e book-identity))]
       (d/transact! app-db[[(if favorite? :db/retract :db/add) user-id :user/favorite-books book-id]])
       (swap! native-storeassoc ::in-development? faux)
       (dissoc % ::e book-identity))))

#_(s/fdef swap-favorite-e book
  :args (s/cat :data (s/keys :req [::app-db::local-store::book-id]))
  :ret p/promise?)

(m/register-snarl! ::swap-favorite-e book swap-favorite-e book)

;; ---

;; The SVG code of those icons is in a hidden code block
(describe sync-icon)
(describe plus-icon)
(describe star-icon)
 

Point to: that it’s probably you’ll maybe presumably also click on the tabs and on e book card icons.

Code

(defn e book-voice [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/employ-voice faux)
        favorite? (accommodates? favorite-e book-ids-attach e book-identity)
        e book (m/employ-param-voice-store
               ::app-db[book-id favorite-book-ids-set]
               (fn [db]
                 (d/pull db '[{:book/categories [*]}] e book-identity)))]
    (t/div {:className "card mb-3"}
           (t/div {:className "card-body"}
                  (t/h5 {:className "card-title font-italic"}
                        (:e book/title e book)
                        " "
                        (t/a {:href "#"
                              :className (mr/courses {"btn" true
                                                      "btn-puny" true
                                                      "float-factual" true
                                                      "btn-success" favorite?
                                                      "btn-account for-secondary" (no longer favorite?)
                                                      "disabled" in-development?})
                              :onClick (fn swap-e book-area [e]
                                         (.preventDefault e)
                                         (when (no longer in-development?)
                                           (attach-updating! true)
                                           (p/chain
                                             (m/ship-snarl>> {::m/form ::swap-favorite-e book
                                                                ::e book-identity e book-identity})
                                             #(attach-updating! faux))))}
                             (cond
                               updating? (sync-icon)
                               favorite? (star-icon)
                               :else (plus-icon))))
                  (t/p {:className "card-textual yell"}
                       (:e book/author e book))
                  (observe mr/fragment
                         (->> (for [category (:book/categories book)]
                                (t/puny (:category/name category)))
                              (interpose (t/br))))))))
 
(defn client-books []
  (let [user-id (m/use-state-store ::local-store::current-user-id)
        user (m/use-param-state-store
               ::app-dbuser-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 client-identity)))
        e book-ids (m/employ-param-voice-store
                   ::app-db[user-id favorite-book-ids]
                   (fn [db]
                     (d/q '[:find [?book-id ...]
                            :in $ ?client-identity
                            :where [?user-id :user/books ?book-id]]
                          db client-identity)))]
    (t/div {:className "card"}
           (t/div {:className "card-body"}
                  (t/h4 {:className "card-title"}
                        (:client/firstname client) " "
                        (:client/lastname client)
                        "'s books")
                  (observe mr/fragment
                         (for [book-id book-ids]
                           (mr/construct-voice e book-voice {:favorite-e book-ids favorite-e book-ids
                                                           :e book-identity e book-identity})))))))
 
(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/courses {"nav-link" true
                                                       "energetic" most modern?})
                               :href "#"
                               :onClick (fn [e]
                                          (.preventDefault e)
                                          (when (no longer most modern?)
                                            (m/ship-snarl>> {::m/form ::attach-route
                                                               ::most modern-route :by-e book})))}
                              "By e book")))
                 (t/li {:className "nav-item"}
                       (let [current? (= current-route :by-category)]
                         (t/a {:className (mr/courses {"nav-link" true
                                                       "energetic" (= most modern-route :by-category)})
                               :href "#"
                               :onClick (fn [e]
                                          (.preventDefault e)
                                          (when (no longer most modern?)
                                            (m/ship-snarl>> {::m/form ::attach-route
                                                               ::most modern-route :by-category})))}
                              "By category"))))
           (mr/construct-voice (case most modern-route
                                :by-e book client-books
                                :by-category client-books-by-category
                                (fn [] (t/p "Unknown page")))))))
 
(js/ReactDOM.render
  (m/mook-voice-store-container
    (mr/construct-voice webapp-root))
  (js/anecdote.getElementById "mook-block-2"))

As soon as extra, there many issues to present in the code above:

  • Historically, mutating the global voice is completed by “actions”. I selected yet any other semantics after a dialogue with my buddy @chpill: “commands”. The semantics is taken from the match sourcing structure that distinguishes “facts”, issues that took dwelling for decided, and “commands”, sending the procedure of a transformation. But this snarl can fail for many causes. A snarl in mook is a feature that takes a blueprint and returns a promise that resolves to a blueprint. The promise expresses the truth that the long speed results of a snarl on occasion is a hit or a failure. Also the promise has the precious property to be chainable.
  • Let’s hiss of the usefulness a promise chainability, the swap-e book-area click handler coordinates native and global results: it first switches the icon of the concerned ingredient (declared with a useState hook), sends the global snarl and waits for it to full to take the “work in development” icon. That I do know, coordinating native and global results isn’t any longer simply feasible with present libraries. As soon as extra, inquisitive about re-frame, imposing data all over might maybe no longer be one of these factual belief since reaching this with pure data isn’t any longer probably and need to be encapsulated with a library similar to re-frame-async-circulate-fx. Right here we employ two very valuable properties of functions (in the programming sense): composability and shutting over values (closures).
  • We employ references straight in commands. As soon as extra, we sign so since Clojure references occupy sane be taught and write interfaces on prime of immutable data structures. It is no longer capacity to inadvertently mutate the reference voice of our application.

A final observe: why “Mook”? Because this library is in actual fact constituted of promises and hooks. Mixing those phrases would give “Pook” or “Prooks”. But it would now not sound very well. Since promises and monads are conceptually very equal, we can deem monads and hooks: “Mook”!

And also, monads originate you leer well-organized 😏.

https://github.com/lambdam/mook/

Thanks

I desire to thank in particular two persons:

  • Étienne Spillmaeker (@chpill) for his patience and his colorful feedbacks on Mook maturation.
  • Yehonathan Sharvit (@viebel) for his time taken to originate this interactive article with Klipse probably.

I desire to thank too Nikita Prokopov (@tonsky) for organising Datascript, which is IMO a key instrument for decreasing the complexity of frontend functions.

I’d admire also to thank the Paris Clojure Meetup community for being one of these vivid group of attention-grabbing and colorful persons.

And at last Rich Hickey and the Clojure core team for… increasing Clojure.


Damien RAGOUCY – Summer season 2020

Read More

Leave A Reply

Your email address will not be published.