Fork me on GitHub
#re-frame
<
2017-11-09
>
andrea.crotti11:11:58

in my re-frame SPA I wanted to add filters in the urls so that urls can be shared around

andrea.crotti11:11:05

and filters stay in

andrea.crotti11:11:47

it's not hard to do I think but I think it should be a very common scenario to solve, is there anything semi-ready to use?

andrea.crotti11:11:21

it's easy to make re-frame overwrite a value in the db from what I get from the URL query arguments

andrea.crotti11:11:32

however I would like to have it synced back if someone changes the filter with the UI

andrea.crotti11:11:44

so in that case it gets a bit more complicated

andrea.crotti11:11:41

that looks good but not sure I can add that dependency since it's not my project

andrea.crotti11:11:38

and how does that help with the bidirectional sync between Re-Frame and the url params anyway?

andrea.crotti11:11:43

not sure it would help so much

andrea.crotti12:11:47

I think with these 3 little functions

(defn get-url
  []
  (-> js/window
      .-location
      .-href
      url))

(defn get-date-from-url
  []
  (-> (get-url)
      :query
      (get "date")))

(defn update-url
  [date]
  (let [url (get-url)
        query (:query url)]
    (str
     (assoc url :query (assoc query "date" date)))))

andrea.crotti12:11:50

I have everything I need

andrea.crotti12:11:15

the only thing missing however is to actually overwrite the url as part of a reframe event

odge12:11:28

What I realized the hard way is that having the event from changing filter settings only update the URL makes for a much less complex solution.

odge12:11:45

Then hook popstate to dispatch a re-frame event that sets the parameters of the url in the db

odge12:11:45

That way the data flows in one direction. Trying to keep the db and url in sync caused us a lot of issues before we did it this way.

manutter5112:11:16

@kenny I always use nested data structures. For one thing, it makes it easier to modularize initial setup of the app db: I can put all my auth-related data into a db.auth namespace, and then my top-level init fn can do something like

(reset! app-db {
  :auth (db.auth/init-db)
  ;; ...
}

manutter5113:11:06

where db.auth/init-db is just (def init-db {}

manutter5113:11:44

I don’t think flat vs nested makes a big difference either way, performance-wise, but I like it much better from a design perspective.

jhund13:11:59

@kenny I am not aware of significant performance differences between flat and nested db. I nest the db primarily for organizational reasons. I’d be curious how others organize the app-db. Below is an overview of how I typically structure it:

jhund13:11:23

:entitities contains normalized data records from my server DB. I use [subgraph](https://github.com/vimsical/subgraph) to manage records. Under the :server key I store data related to server interaction. E.g., the key [:server :data :state] is used to render a spinner.

jhund13:11:24

Under the :ui key I store data related to the user interface. Each entry can have a [:components] key to store data for further sub-components, and a [:data] key to store data for the current ui component.

jhund13:11:48

E.g., the path [:ui :data :active-panel] stores which panel is currently shown.

jhund13:11:01

The last key is [:user]. Here I store data related to the current user. In this case only the :user/id in a format that is compatible with subgraph so that I can load details via subgraph from under the :entities key.

jhund13:11:26

I don’t have a ton of experience with re-frame, so I welcome any feedback.

raymcdermott16:11:37

I want to include a react QR code PNG generator written in JS in my re-frame app

raymcdermott16:11:05

is working with JS react components a well-trodden path?

raymcdermott16:11:48

though obvs I mean any react component

raymcdermott16:11:26

I am confused by needing to npm install his code … isn’t that weird?

mikerod16:11:43

@raymcdermott I think it is “trodden”ed to some extent at least

mikerod16:11:55

https://clojurescript.org/guides/javascript-modules is something I haven’t tried out yet to directly work with npm

mikerod16:11:04

Under section “Node Modules”

mikerod16:11:39

Another way I’ve achieved bringing in something like this before was by finding if there is a way to get it as a single js file that you can just copy into your project and point to via :foreign-libs and then deal with any externs issues

mikerod16:11:04

but the new compiler more direct support for npm deps may be a smoother route now

raymcdermott16:11:13

actually have found a service that will do it for me

raymcdermott16:11:20

@lovuikeng thanks for the link

raymcdermott16:11:10

@mikerod excellent - thanks

lovuikeng16:11:09

cool, free of charge too, good to know, thanks @raymcdermott

mikerod17:11:19

I believe I already know this, but is it acceptable to return nil from a event handler registered via reg-event-db

mikerod17:11:54

I believe the return value is just put under the :db effect and nil just means do not update the db?

mikerod17:11:20

actually, It may replace it with nil now that I look at it. I’m mostly just asking about if it conceptually wrong etc too.

mikerod17:11:36

I’m not sure that the :db effect with nil works out well though

mikerod17:11:08

but thanks for the link. I was just checking on that doc again. I’ve read it all before

lovuikeng17:11:52

yes, should work, further down the doc, the magic comes from "an interceptor, cunningly named do-fx"...

lovuikeng17:11:57

I know, @mikerod I think we all go back to re-frame readme all the time 🙂

mikerod17:11:37

I like the docs a lot

mikerod17:11:55

but yeah, definitely re-read them on occasion

kenny18:11:32

@manutter51 @jhund The performance benefit comes into play when re-rendering. All top level subscriptions rerun whenever the app DB changes. If you have nested subscriptions then you can avoid rerunning additional subscriptions when the app DB changes.

jhund18:11:54

@kenny I’m not sure I follow. As far as I understand, top level subscriptions will always run when anything in app-db changes. No matter how you structure your data.

jhund18:11:20

the performance benefit comes from cascading subscriptions (levels higher than 1) where the inputs to a subscription are other subscriptions, not the entire app-db.

kenny18:11:40

That is precisely what I am suggesting.

jhund19:11:07

Yes, after re-reading your message I realize that now 🙂

jhund19:11:30

For some reason I thought you were suggesting that nesting the data in app-db would give you a performance benefit. However, you never said that. Sorry.

kenny19:11:00

In my mind, the nesting implies cascading subscriptions.

manutter5119:11:22

Ah, ok, I understand now. I use both, but I don’t always use cascading subscriptions where I have nested values, or vice versa

kenny19:11:39

But you probably should 🙂

manutter5119:11:03

Not necessarily. If you have composite data that changes as a unit, there’s no benefit to cascading subscriptions, and also you can have cascading subscriptions of purely derived data that don’t correspond to what’s stored in the db

jhund19:11:51

I agree, the nesting of data in app-db and the cascading of subscriptions seem to be largely independent.

danielcompton19:11:24

@kenny you could create a subscription :login-state which does a select-keys on app-db if you wanted. That would then only rerun :login-state when app-db changes which would be pretty cheap. And then nest everything off that

kenny19:11:16

Really the question was more of how people typically structure their app state in a way that scales.

kenny19:11:40

A flat DB is nice because you don't have to have a way to navigate various DB paths.

kenny19:11:59

But that comes at the expense of rerunning every subscription each time the DB changes.

jhund21:11:05

@kenny I think there is still a disconnect. Whether every subscription is re-run each time the DB changes has nothing to do with how app-db is structured, but everything with how you cascade subscriptions.

kenny21:11:36

Still saying that 😉

jhund21:11:35

When you say “But that comes at the expense of rerunning every subscription each time the DB changes”, what do you mean with “that”?

jhund21:11:39

A flat DB?

jhund21:11:55

And is “flat DB” referring to the data structure inside your app-db?

kenny21:11:39

The "that" in that sentence is referring to my statement right above - the flat DB 🙂

kenny21:11:54

And yes DB = app-db.

jhund21:11:10

Are you saying that a flat DB means you have to rerun every subscription each time the DB changes?

kenny21:11:46

Yes, given that all subscriptions are simple gets off the DB.

jhund21:11:47

trying to get this sorted out 🙂

kenny21:11:58

If you had a subscription that takes one of the get subscriptions as an input signal, then that may or may not be rerun. But that is not relevant to my original question.

jhund21:11:28

“all subscriptions are simple gets off the DB” is the only factor determining whether all subscriptions run every time anything in app-db changes.

jhund21:11:40

The structure of app-db has nothing to do with it.

jhund21:11:16

However the statement “that a flat DB means you have to rerun every subscription each time the DB changes” implies that it has to do with structure of app-db

jhund21:11:31

I keep getting the sense that you suggest the structure of app-db (flat or nested) determines which subscriptions are run when anything in app-db changes.

jhund21:11:52

Based on statements like “a flat DB means you have to rerun every subscription each time the DB changes”

kenny21:11:10

That is not true. But the way you write your subscriptions is often dependent on the structure of the DB.

kenny21:11:08

I think a concrete example would help here. Take this DB:

{:a ""
:b ""
:c/c1 ""
:c/c2 ""}
If you had a subscription for each value in your DB then every time :a, :b, :c/c1, or :c/c2 changes, every subscription is ran. If instead you structured your DB like this:
{:a ""
:b ""
:c {:c1 ""
:c2 ""}}
where the :c subscription is an input signal for :c1 and :c2, when the app DB update only subscriptions :a, :b, and :c are ran. Before subscriptions :a, :b, :c/c1, or :c/c2 were all ran.

jhund21:11:52

Ok, with a nested DB it’s easier to write a Level 2 (extraction) subscription (https://github.com/Day8/re-frame/blob/master/docs/SubscriptionInfographic.md) if my input signal is a single sub tree of app-db vs. a list of attrs (in a flat db).

kenny21:11:05

While the performance of this small problem is negligible, when you scale it up to a production app of 100s of those the perf starts to become apparent.

jhund21:11:26

Yes, it all makes sense to me now. Thanks for clarifying 🙂

jhund21:11:57

The implication of simpler level 2 subscriptions for nested DBs was the missing part.

kenny21:11:40

That's what I was trying to get across in my original code sample with input signals:

(rf/reg-sub
  :login-state
  (fn [db _]
    (:login db)))

(rf/reg-sub
  :email
  :<- [:login-state]
  (fn [state]
    (:email state)))

(rf/reg-sub
  :password
  :<- [:login-state]
  (fn [state]
    (:password state)))

jhund21:11:52

That was a long time ago 😉

jhund21:11:10

I lost that context.

jhund21:11:16

Thanks again

kenny21:11:25

Gotcha, no worries. I had a feeling we were going in circles 🙂

danielcompton22:11:51

One way to split the difference is:

{:email ""
 :password "sekrit"}

(rf/reg-sub
  :login-state
  (fn [db _]
    (select-keys db [:email :password])))

(rf/reg-sub
  :email
  :<- [:login-state]
  (fn [state]
    (:email state)))

(rf/reg-sub
  :password
  :<- [:login-state]
  (fn [state]
    (:password state)))

danielcompton22:11:25

Then you have your keys at the top of app-db, but only a few subscriptions will fire on change

danielcompton22:11:37

I'd probably still recommend some light nesting, but is another way to do it

mikerod23:11:46

I’m glad this question was asked. I was having similar concerns before. Enlightening

mikerod23:11:58

This is sort of the conclusion I was coming to as well

mikethompson23:11:12

@kenny @jhund I'd summarize the efficiency considerations like this ... 1. avoid a large amount of computation in any Level 2 subscription. Make them trivial (just getters). In which case, it doesn't much matter how many of them there are (within reason), because running them takes no time. 2. "propagation pruning" (of values flowing through the subscription signal graph) happens when the value of a subscription this time tests = to the value last time 3. So, to efficiently prune propagation, make the = checking efficient. (otherwise = tests could take some time). Ideally, you want the = test to succeed quickly or fail quickly. 4. Because = does a shortcutting identical? check, the best way to get fast = succees is to make last time test identical? to this time.

mikethompson23:11:48

I edited the above