Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Most Efficient Design Layouts? #304

Closed
caryfitzhugh opened this issue Dec 29, 2016 · 9 comments
Closed

Most Efficient Design Layouts? #304

caryfitzhugh opened this issue Dec 29, 2016 · 9 comments

Comments

@caryfitzhugh
Copy link

Hello!

I've got a project with a large number of elements in a hash.

And reagent renders a list from them.

Wondering what is more efficient -- to make a number of dynamic subscriptions or have one subscription and pull the data from it in the view?

(defn my-view [item-id]
   (let [all (subscribe :all-items)]
     (fn []
        [:h1 (:name (get @all item-id)])))

Or something like this:

(defn element [item-id]
   (let [element (subscribe :dynamic-subscriber item-id)]
     (fn []
       [:h1 (:name @element))])))

Any general thoughts? I am keeping the amount of stuff passed into the components smallish - ids and things. Just wondered if there was a substantial gain using either approach. I go back and forth though in my head - because dynamic subscribers seem to need to be created / destroyed / updated / cached, while "get" just pulls out of a hash. On updates I'm guessing there is a great benefit.

And if this is in the docs, please pardon me - the docs have recently changed quite a bit!

Additionally, i was unable to find a way to join the re-frame slack channel (it must be some type of common knowledge that I'm unaware of) - so maybe this question would be better asked there?

Thanks!

@thenonameguy
Copy link
Contributor

Generally speaking, using subscriptions for more 'exact' data will be more performant (so the latter example in your case) and usually it encapsulates your filtering logic better.

Your render function would run whenever :all-times returns a new value (even if your selection doesn't change). In the latter case when the DB updates, only the :dynamic-subscriber function body will be rerun, and only if that returns a new (selected) value, the render function will be run again.

@mike-thompson-day8
Copy link
Contributor

mike-thompson-day8 commented Dec 29, 2016

Join clojurians via: http://clojurians.net/

Yes, as @thenonameguy says, the first variation will rerender each item every time there is a change to the all-items collection. That's probably inefficient if only one of them has changed.

You will need something which renders the entire list:

(defn items-list
  []
  (let [items @(subscribe [:all-items])]
    [:div
     (for [i items]
       (let [id (:id i)]
         ^{:key id} [item-renderer id]))]))

This renderer will rerun each time there is a change to the items collection. But we oganise for it to do as little rendering as possible. And all items have a key and we simply pass down the id of the item to reender.

Then we need to write the item-render

(defn item-renderer
  [id]
  (let [item  @(subscribe [:item id])]     ;; obtain the individual item
    [:div  (:name item)]))

This assumes there are two susbcriptions:

  • one which delivers all items
  • one which will deliver a single item. given a key.

Of course, there are variations on this. You could pass in the entire item to the item render, rather thsan the id. And then the item renderer doesn't have to source it again internally. But that will means Reagent has to do equality checks on each item, on each render. Which is not a bad strategy if the list is fairly stable, but if items are getting added and removed near the top, that probably sucks.

@si14
Copy link
Contributor

si14 commented Dec 29, 2016

Personally I found it handy to have both an ordered collection (i.e. a vector) and keyed one (i.e. a map). On the other hand, having the same "object map" in two places is cumbersome because you suddenly need to do updates in two places. Thus I use the following pattern quite often:

(def default-db
  {:items [3 42 1]
   :by-id/items {3 {:id 3
                    :name "Foo"}
                 42 {:id 42
                     :name "Bar"}
                 1 {:id 1
                    :name "Baz"}}})

(reg-sub
 :item-ids
 (fn [db _]
   (:items db)))

;; can be made marginally more effective by using :by-id/items 
;; subscription instead of the full DB (does it? Would love comments
;; by the team!)
(reg-sub
 :item
 (fn [db [_ id]]
   (get-in db [:by-id/items id])))

;; in case you need a "joined" subscription, too
(reg-sub
 :by-id/items
 (fn [db _]
   (:by-id/items db)))

(reg-sub
 :items
 :<- [:item-ids]
 :<- [:by-id/items]
 (fn [[item-ids items-by-id] _]
   (mapv items-by-id item-ids)))


(defn item [id]
  (let [item @(subscribe [:item id])]
    [:li (:name item)]))

(defn items-list []
  (let [item-ids @(subscribe [:item-ids])]
    [:ul
     (for [id item-ids]
       ^{:key id} [item id])]))

This particular snippet is completely untested, but I hope you get the idea.

@mike-thompson-day8
Copy link
Contributor

@si14 yeah, nice approach.

@mike-thompson-day8
Copy link
Contributor

mike-thompson-day8 commented Jan 17, 2017

@si14 if ever you wanted to put together a PR for a docs page (in repo's /docs) on this, or a blog post, I'd like to link to it. I have a feeling this is practically an FAQ.

@p-himik
Copy link
Contributor

p-himik commented Feb 6, 2017

@si14 Suppose you have multiple :items-like collections of IDs that may reside somewhere deep with a dynamic path (e.g. page->parent-element->children-ids). And :by-id/items can potentially store a very large number of elements.
How would you detect that a particular item is no longer needed? I.e. it's not referenced by any ID collection.

@mike-thompson-day8
Copy link
Contributor

As far as I can tell, I'm okay to close this issue.
See also the library subgraph.

@martinklepsch
Copy link
Contributor

martinklepsch commented Feb 17, 2018

For those who are interested, a link to the mentioned subgraph library.

@mike-thompson-day8
Copy link
Contributor

Libraries are listed in an FAQ:
https://github.com/Day8/re-frame/blob/master/docs/FAQs/DB_Normalisation.md

If anyone creates new a solutions, can you supply a PR updating that FAQ document please.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants