The defacto
state store library. A lightweight,
highly customizable state store for clojure(script). You've heard of re-frame or redux? Same patterns. New glue.
;; deps.edn
{:deps {skuttleman/defacto {:git/url "https://github.com/skuttleman/defacto"
:git/sha "{SHA_OF_HEAD}"}}}
(require '[defacto.core :as defacto])
(require '[clojure.core.async :as async])
;; make some command handlers
(defmethod defacto/command-handler :stuff/create!
[{::defacto/keys [store] :as ctx-map} [_ command-arg :as _command] emit-cb]
(do-stuff! ctx-map command-arg)
;; command handlers can do work synchronously and/or asynchronously
(async/go
(let [result (async/<! (do-more-stuff! ctx-map command-arg))]
(if (success? result)
(do
(async/<! (do-more-more-stuff! ctx-map command-arg))
(emit-cb [:stuff/created command-arg]))
(defacto/dispatch! store [:another/command! result])))))
;; make some event handlers
(defmethod defacto/event-reducer :stuff/created
[db [_ value :as _event]]
(assoc db :stuff/value value))
;; make some subscription handlers
(defmethod defacto/query-responder :stuff/?:value
[db [_ default]]
(or (:stuff/value db) default))
;; make a store
(def my-store (defacto/create {:some :ctx} {:stuff/value nil}))
;; make a subscription
(def subscription (defacto/subscribe my-store [:stuff/?:value 3]))
;; dispatch a command
(defacto/dispatch! my-store [:stuff/create! 7])
;; emit an event
(defacto/emit! my-store [:some/event {...}])
I love reagent, and I use it for all my cljs UIs. Making a
reactive reagent
store with defacto
is super easy!
(ns killer-app.core
(:require
[cljs.core.async :as async]
[cljs-http.client :as http]
[defacto.core :as defacto]
[reagent.core :as r]
[reagent.dom :as rdom]))
(def component [store]
(r/with-let [sub (defacto/subscribe [:some/?:page-data 123])]
(let [{:keys [status data error]} @sub]
[:div.my-app
[:h1 "Hello, app!"]
[:button {:on-click #(defacto/dispatch! store [::fetch-data! 123])}
"fetch data"]
[:div
(case status
:ok data
:bad error
"nothing yet")]])))
;; anything can go in the `ctx-map`, such
(defn app-root [] ;; as fns you may want to mock/stub in tests
(r/with-let [store (defacto/create {:http-fn http/request} {:init :db} {:->sub r/atom})]
;; using [[r/atom]] gets you
;; **reactive subscriptions**!!
[component store]))
(rdom/render [app-root] (.getElementById js/document "root"))
(defmethod defacto/command-handler ::fetch-data!
[{::defacto/keys [store] :keys [http-fn]} [ id] emit-cb]
(async/go
(let [result (async/<! (http-fn {...}))
;; deref-ing the store is NOT reactive and can be used inside command handlers
current-db @store
;; then you can query the db directly instead of using reactive subscriptions
page-data (defacto/query-responder current-db [:some/?:page-data])]
(do-something-with page-data)
(if (= 200 (:status result))
(emit-cb [::fetch-succeeded {:id id :data (:body result)}])
(emit-cb [::fetch-failed {:id id :error (:body result)}])))))
(defmethod defacto/event-reducer ::fetch-succeeded
[db [_ {:keys [id data]}]]
;; query the db from events or other queries
(let [current-data (defacto/query-responder db [:some/?:page-data id])]
(assoc-in db [:my-data id] (merge current-data {:status :ok :data data}))))
(defmethod defacto/event-reducer ::fetch-failed
[db [_ {:keys [id error]}]]
(assoc-in db [:my-data id] {:status :bad :data error}))
(defmethod defacto/query-responder :some/?:page-data
[db [_ id]]
(get-in db [:my-data id]))
defacto
uses a "plugin" architecture so functionality can be extended by including modules. Here are a few to get
you started.
- defacto-forms+ - this combines
defacto-res
anddefacto-forms
to help you link user-input with an asynchronous resource. - defacto-res - a low-level module for managing asynchronous "resources" in your state store
- defacto-forms - a simple isolation layer for managing arbitrary user-input
The design is vaguely CQS (like most state stores).
Here are the concepts and conventions that defacto
uses.
command
- a message "dispatched" through a defacto
store via defacto.core/dispatch!
. Commands are the primary
mechanism for "getting stuff done". They can cause any side effect you need (such as emitting events). A command
should be a vector with a keyword in first position. Prefer qualified keywords which are present-tense verbs
ending with a !
.
action
- the keyword in first position of a command
vector.
handler
- the implementation for responding to a specific command
action
.
;; example command
[:my.domain/create-thing! {:thing :params}]
event
- a message "emitted" through a defacto
store via dispatching a command
(or defacto.core/emit!
for convenience).
Events are the only way to update defacto
's internal db. An event
should be a vector with a keyword in first position.
Prefer qualified keywords which are past-tense verbs.
event-type
- (aka type
) is the keyword in first position of an event
vector.
reducer
- the implementation for updating the db value in response to a specific event
type
. The reducer
must be a pure function.
;; example event
[:my.domain/thing-created {:thing :details}]
query
- a message used to request data from defacto
's internal db. They can be used to subscribe to the internal
db's updates. A query
should be a vector with a keyword in first position. Prefer qualified keywords which are
nouns. I like prefixing the name with ?:
to make them stand out more in code and autocomplete in my editor.
resource
- the keyword in first position of a query
vector.
responder
- the implementation for finding data in the db relevant to a specific query
resource
. The responder
must be a pure function.
subscription
- a deref-able, watch-able ref type that will update when the result of a query changes.
;; example query
[:my.domain/?:thing {:id 123}]
sequenceDiagram
participant ...
APP-->>STORE: subscribe! (data?)
...->>APP: user interaction, HTTP, WS, etc
APP->>STORE: dispatch! command
loop
STORE->>handler: command
handler->>...: do stuff!
handler->>STORE: dispatch! command(s)
end
handler->>STORE: emit! event(s)
STORE->>reducer: DB, event
reducer->>STORE: DB
STORE-->>APP: updated sub (data)
Good question. re-frame is awesome, but it's usually too heavy-weight for my purposes. Sometimes I just want to build things out of tiny, composable pieces.