-
Notifications
You must be signed in to change notification settings - Fork 45
Re frame Integration
Warning! This is based on posh version [posh “0.3.5”] [datascript “0.15.0”] [re-frame “0.7.0”]
the latest versions of both posh and re-frame will not work exactly the same.
First things first make sure you’ve got what you need
(ns posh-re-frame
(:require
[reagent.core :as reagent]
[re-frame.core :refer [subscribe dispatch register-handler register-sub]]
[posh.core :refer [posh!] :as posh]
[datascript.core :as d])
(:require-macros
[reagent.ratom :refer [reaction]]))
setup your conn the normal way
(def schema {:project/todos {:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many}})
(defonce conn (d/create-conn schema))
(posh! conn)
(register-handler
::add-conn
(fn [db [_ conn]]
(assoc db :ds conn)))
(dispatch [::add-conn conn])
Now you’ve got a posh’d connection accessible in all your subscriptions
Posh’s queries and pull’s return reactions, so you can just return them wherever you’d otherwise return a reaction
as an example, here is a subscription that just returns the value of the whole app-db the standard re-frame way, and a subscription using posh’s q to give you the full value of all the entities in your db
(register-sub
::all
(fn [db]
(reaction @db)))
(register-sub
::all-ents
(fn [db]
(let [conn (:ds @db)]
(posh/q conn '[:find (pull ?e [*])
:where [?e]]))))
another option is of course to just ignore the app-db all together in that case, if all you wanted was the “appearance” of re-frame, you could just pass the conn in as an argument to your subscriptions
(register-sub
:e
(fn [_ [_ conn eid]]
(posh/pull conn '[*] eid)))
(register-sub
:todos
(fn [db [_ conn]]
(posh/q conn '[:find ?e
:where [?e :todo/title]])))
;; if you take that route, don't forget that values passed around the app should be passed down as far as they get used,
;;THROUGH the inner function as well!
(defn todo [conn eid]
(let [todo (subscribe [:e conn eid])]
(fn []
[:div (pr-str @todo)])))
(defn project [conn]
(let [all (subscribe [:todos conn])]
(fn [conn]
[:div
(for [[n] @all]
[todo conn n])])))
;; and at the top of the project you just add the conn in.
(defn mount-root []
(reagent/render [project conn]
(.getElementById js/document "app")))
Another thing to be aware of is that re-frame subscriptions can take a 2nd argument, a vector of signals which will cause the reaction to re-render whenever they change.
So, let’s say you wanted to display a given entity, based on some value in your app-d, and you want to use posh’s pull. You want to pull a different entity whenever that external value changes, but in the case where some attribute of the entity changes while you’re still viewing it, you want that change to be reflected as well.
In that case, dynamic subscriptions come to the rescue.
(register-sub
::position
(fn [db]
(reaction (:position @db 0))))
(register-sub
::active-entity
(fn [db _ [eid]]
(let [conn (:ds @db)]
(posh/pull conn '[*] eid))))
(register-handler
::inc-position
(fn [db]
(update db :position inc)))
(fn dynamic-example []
(let [id (subscribe [::position])
entity (subscribe [::active-entity] [id])]
(fn []
[:div
[:h1 "These are all the attributes for the entity with id of:" @id]
[:div (pr-str @entity)]
[:button {:on-click #(dispatch [::inc-position])}
"Change value in app-db"]
[:button {:on-click #(dispatch [::transact
[{:db/id @id
:text "This just got added"}]])}
"Transact something to the posh'd conn"]])))
Notice, in a dynamic subscription, you don’t deref anything in the second argument vector. You’re passing in a reaction, but re-frame magic is derefing it for you and only updating when the value changes.
One of the really nice things about re-frame (to me at least) is how it directs you towards splitting up the logic of your application into views, handlers, and subscriptions.
If you want to abandon this path, and have plain posh queries and datascript transactions all over the place, that’s cool. But personally, I like re-frame’s dispatch and I plan on continuing to use it.
Now, what I left out earlier is that, should you choose, there is nothing to prevent you from using multiple datascript db’s in a single application. After all, it is possible to query multiple databases in a single query, both in datascript and datomic.
So that said, here is a wrapper for datascript’s transact (posh’s transact returns a [:span] tag, and as seen above, we’re listening for datascript transaction results so you can’t have that.
(register-handler
::transact
(fn [db [_ transaction & {:keys [ds-id] :or {ds-id :ds}}]]
(let [conn (get db ds-id)]
(d/transact! conn transaction)
db)))
;usage
(dispatch [::transact [{:db/id 1 :name "Hello everyone"}]])
;hypothical usage with multiple databases -- but please don't actually do it this way
(dispatch [::transact [{:db/id 1 :name "I'm going to a specific datascript db"}]
{:ds-id :dbConor}])
One of the big caveats to this approach is, since you’ve got an atom inside an atom, you’re going to have to be a little cautious whenever you’re serializing your app-state
For instance, if you want to persist your app in local storage, you can’t just do a straight pr-str
- Make sure you deref and pr-str your database connection from within the app-db whenever you’re trying to persist
- You’re going to want to reload the value of the db into the conn that is part of the current app-db, since this is what is posh’d
(defn store! [k v]
(js/localStorage.setItem k v))
(defn load! [k]
(js/localStorage.getItem k))
(defn save-app-to [k db]
(->> (update db :ds #(pr-str @%))
pr-str
(store! k))
db)
;; here, I'm saving the state of the whole app whenever the conn changes
(d/listen! conn :persistence
(fn [tx-report]
(when-let [db (:db-after tx-report)]
(js/setTimeout #(save-app-to "app-db" @app-db) 0))))
(cljs.reader/register-tag-parser! "datascript/DB" datascript.db/db-from-reader)
(cljs.reader/register-tag-parser! "datascript/Datom" datascript.db/datom-from-reader)
(if-let [db (load! "app-db")]
(->> db
cljs.reader/read-string
:ds
cljs.reader/read-string
(d/reset-conn! conn))
(d/transact! conn default-transaction))
;; I'm not using the whole app yet though, just reseting the conn (passed to my app-db above) to be the value of whatever was in the :ds