Fork me on GitHub
#re-frame
<
2020-07-21
>
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
  (:require
   [clojure.string :as str]
   [datascript.core :as d]
   [re-frame.core :as rf]
   [reagent.core :as reagent ]
   [reagent.dom :as dom]))

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

(rf/reg-sub
 ::ds
 (fn [{:keys [ds]}] ds))

(rf/reg-event-fx
 ::transact
 (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])
  (mount))

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

(comment

  (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?

p-himik09:07:57

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

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

p-himik09:07:44

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?

p-himik10:07:38

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!

p-himik11:07:34

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?

p-himik11:07:18

Exactly.

🎉 3
folcon13:07:27

@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.

folcon13:07:04

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.

3
folcon14:07:53

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?

deadghost14:07:36

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?

p-himik17:07:46

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.

coby22:07:49

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