re-frame

https://github.com/Day8/re-frame/blob/master/docs/README.md https://github.com/Day8/re-frame/blob/master/docs/External-Resources.md
2020-07-21T06:39:27.009Z

Yeah, I have to admit, I find the browser API to be good enough for such tasks.

2020-07-21T09:42:03.014200Z

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}]]))

2020-07-21T09:45:05.015400Z

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?

2020-07-29T13:11:27.107200Z

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

2020-07-29T13:21:05.107800Z

It works, really well.

2020-07-29T13:22:43.109900Z

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

2020-07-29T13:25:24.112600Z

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.

2020-07-29T13:27:19.115400Z

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.

2020-07-29T13:33:04.115600Z

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

2020-07-29T13:51:38.117100Z

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.

2020-07-29T13:51:58.117600Z

Immutability rules.

1
2020-07-29T14:09:53.117900Z

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

2020-07-29T14:29:09.118600Z

No problem :) Also message me if it works!

p-himik 2020-07-21T09:53:57.015500Z

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-himik 2020-07-21T09:55:44.015700Z

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.

2020-07-21T10:00:01.018200Z

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.

2020-07-21T10:00:50.019200Z

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

2020-07-21T10:01:55.021Z

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-himik 2020-07-21T10:05:38.021200Z

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.

2020-07-21T11:15:06.021600Z

Why would that be?

2020-07-21T11:18:28.023500Z

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-himik 2020-07-21T11:23:34.023700Z

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.

p-himik 2020-07-21T11:51:18.027100Z

Exactly.

1๐ŸŽ‰
deadghost 2020-07-21T14:50:36.030600Z

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?

deadghost 2020-07-21T15:17:54.030700Z

relevent: https://stackoverflow.com/questions/46176515/clojurescript-re-frame-subscription-dereferencing-dilemma so I guess it's perfectly fine?

p-himik 2020-07-21T17:11:46.031200Z

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.

coby 2020-07-21T22:52:49.032100Z

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!

25๐ŸŽ‰