Fork me on GitHub
David Pham09:07:03

Hello everyone. I might ask a stupid question, so please forgive me. The context of my question is the following: I would like to use the datalog query engine with re-frame. To that end, I turned to datascript. I know that re-posh and posh exists to fulfill that goal and that you can combine them. However, I would like to understand why we could not use the normal re-frame workflow: datascript database are immutable values. So we could store multiple datascript database/structure in our re-frame app-db in a given path (say [:ds]) and apply transaction to that immutable value and replace it. My understanding is that subscription should work as intended as since whenever we modify datascript datastore under the [:ds]` path, there would be reactions computed by the re-frame data flow. I attached some code to display the behavior.

(ns play.core
   [clojure.string :as str]
   [datascript.core :as d]
   [re-frame.core :as rf]
   [reagent.core :as reagent ]
   [reagent.dom :as dom]))

 (fn [_ _]
   {:db {:ds (d/empty-db {:aka    {:db/cardinality :db.cardinality/many}
                          :name   {:db/unique :db.unique/identity}
                          :friend {:db/valueType :db.type/ref}})}}))
 (fn [db _]
   (when (:ds db)
     (d/datoms (:ds db) :eavt))))

 (fn [{:keys [ds]}] ds))

 (fn [{db :db} [_ data]]
   {:db (assoc db :ds (d/db-with (:ds db) data))}))

(defn app []
  (let [datoms (rf/subscribe [::datoms])]
    (fn []
      [:div {:style {:width "100%"}}
       [:pre {:white-space :pre-wrap
              :word-wrap :break-word}
        (into [:<>] (interpose [:br] @datoms))]])))

(defn mount []
  (dom/render [app] (.getElementById js/document "app")))

(defn main []
  (rf/dispatch [::init])

(defn ^:dev/after-load reload []


  (rf/dispatch [::transact
                [{:name  "Mickey", :age 32, :aka ["A" "B" "C"], :friend 10}
                 {:name  "Mimi", :age 10, :employed? true, :married? true}]])

  (rf/dispatch [::transact
                [{:name  "Hello", :age 15, :aka ["X" "Y" "Z"], :friend 2}
                 {:name  "World", :age 37, :employed? true, :married? false}]])

  (rf/dispatch [::transact
                [{:name  "Pietr", :age 15, :aka ["X" "Y" "Z"], :friend 2}
                 {:name  "Mary", :age 37, :employed? true, :married? false}]])

  (rf/dispatch [::transact
                [{:name  "Mickey", :age 60, :aka ["A" "B" "C"], :friend 10}
                 {:name  "Mimi", :age 14, :employed? true, :married? true}]]))

David Pham09:07:05

So whenever I evaluate the dispatch, I can see the view being recomputed, which achieve the desired behavior. I wonder what would be the drawback?


The ::ds sub couples your views to your data. There's an article about such subs and why they're bad, somewhere in re-frame FAQ I think. Also

 (fn [{db :db} [_ data]]
   {:db (assoc db :ds (d/db-with (:ds db) data))}))
could be written simply as
 (fn [db [_ data]]
   {:db (update db :ds d/db-with data)}))


Note also that if you can store your data as regular maps and query it as well, then using datascript will definitely have a performance impact, maybe a noticeable one. If you use a lot of subs with EQL (I think that's the name), then they will all be re-run each time db (or (:ds db) if you use layer 2 subs) changes.

David Pham10:07:01

Thanks a lot for the advice. My goal was not to have use the query engine for the whole app-state logic, but a few of my views requires queries that are convoluted.

David Pham10:07:50

About subs with EQL, I though only subscriptions that were in use in the actual view would rerun?

David Pham10:07:55

Say view A uses subs :a, and view B uses :b, if users navigate from A to B, then I thought :a would be disposed and not recomputed?


Right, of course - only the ones that are in use. But all of them. :) Meaning, you cannot have many layer-3 subs. (also correction to the above - it should've been "layer 3 subs") To illustrate my point. Consider you have this map:

{:a {:b {:c 1}}}
You can write a layer-2 sub for :a, a layer-3 sub for :b that gets recomputed if and only if the result of sub :a changes, and layer-3 sub for :c that, similarly, get recomputed if and only if the result of sub :b changes. With datascript, unless I'm missing something, you cannot chain subs in such a way.

David Pham11:07:06

Why would that be?

David Pham11:07:28

Thanks a lot. I will try to make my experiments. But I think if the value in app-db is immutable, layer 3 should also work! I will run some experiments! Thanks!


Sure, no problem. > Why would that be? When you run an EQL query, you run it on an instance of a database. The result of a query is some data. You cannot reuse that data in place of a database. Of course, you can lay other subs on top of a EQL sub. It's just that those subs won't be EQL ones. Meaning, you can have only a single EQL layer.

David Pham11:07:10

Okay, so I can do ds -> query+pull of ds [EQL] -> additional transformation from the output data but this can‘t be any EQL (unless what I return is an instance of a datascript data base)? Is this what you meant?



🎉 3

@UEQGQ6XH7, sorry just reading over this. I've been wanting to do something similar for a while, how are you experiments coming?

David Pham13:07:05

It works, really well.

David Pham13:07:43

What I created several datascript/db inside my app-db, and the subscription works swiftly.

David Pham13:07:24

I usually try to have the datascript part in a single sub, and then pipe it into other subs for refinements, but I am really happy with the result. It alleviates me from deciding about a structure inside the app-db, at the cost of defining a schema for the ds and have a few transformation for transacting the entities.

David Pham13:07:19

You use d/db-with for performing transactions, and the rest is typical re-frame. In contrast to re-posh, the way you write the subscription feels more “natural” and you still get access to all the cool stuff from datascript such as access to the indices and all.


Do you have any example code other than what you've got prior to the thread?

David Pham13:07:38

Nope, but it summaries exactly what I did. Put a (d/empty-db) somewhere on your app-state and start to query and transact with it.

David Pham13:07:58

Immutability rules.


I'll give it a go, do you mind if I msg you here if I have an issue?

David Pham14:07:09

No problem :) Also message me if it works!

David Pham11:07:10

Okay, so I can do ds -> query+pull of ds [EQL] -> additional transformation from the output data but this can‘t be any EQL (unless what I return is an instance of a datascript data base)? Is this what you meant?


Are top level (let [foo @(subscribe [:foo])] ... in components a bad idea? I am pretty sure they are but I don't fully comprehend how automagic efficient rerendering works. If foo changes I'd expect the entire component to re-render, but if it's (let [foo (subscribe [:foo])] ... with @foo in certain places, will only relevant parts of the component re-render?


It's not about how you manage your hiccup, it's about how you organize components. If you deref a sub in a component, then it will be rerendered each time the sub changes. Unless it's a form-2 component and you deref the sub outside of the render function. And yes, using @(subscribe ...) is absolutely fine.


Hey, just noticed that Re-Frame officially hit 1.0 a couple days ago, so just wanted drop by and say congrats to the team!

🎉 75