How do I loop through a subscribed collection in re-frame and display the data as a list-item?
Asked Answered
L

3

13

Consider the following clojurescript code where the specter, reagent and re-frame frameworks are used, an external React.js grid component is used as a view component.

In db.cls :

(def default-db
  {:cats [{:id 0 :data {:text "ROOT" :test 17} :prev nil :par nil}
          {:id 1 :data {:text "Objects" :test 27} :prev nil :par 0}
          {:id 2 :data {:text "Version" :test 37} :prev nil :par 1}
          {:id 3 :data {:text "X1" :test 47} :prev nil :par 2}]})

In subs.cls

(register-sub
  :cats
  (fn [db]
    (reaction
      (select [ALL :data] (t/tree-visitor (get @db :cats))))))

result from select:

[{:text "ROOT", :test 17} 
 {:text "Objects", :test 27} 
 {:text "Version", :test 37} 
 {:text "X1", :test 47}]

In views.cls

(defn categorymanager []
      (let [cats (re-frame/subscribe [:cats])]
         [:> Reactable.Table
             {:data (clj->js @cats)}]))

The code above works as expected.

Instead of displaying the data with the react.js component I want to go through each of the maps in the :cats vector and display the :text items in html ul / li.

I started as follows:

(defn categorymanager2 []
      (let [cats (re-frame/subscribe [:cats])]
         [:div
           [:ul
             (for [category @cats] 
;;--- How to continue here ?? ---
        )
        ))

Expected output:

ROOT
Objects
Version
X1

How do I loop through a subscribed collection in re-frame and display the data as a list-item? ( = question for title ).

Leaf answered 11/5, 2016 at 13:25 Comment(0)
M
39

First, be clear why you use key...

Supplying a key for each item in a list is useful when that list is quite dynamic - when new list items are being regularly added and removed, especially if that list is long, and the items are being added/removed near the top of the list.

keys can deliver big performance gains, because they allow React to more efficiently redraw these changeable lists. Or, more accurately, it allows React to avoid redrawing items which have the same key as last time, and which haven't changed, and which have simply shuffled up or down.

Second, be clear what you should do if the list is quite static (it does not change all the time) OR if there is no unique value associated with each item...

Don't use :key at all. Instead, use into like this:

(defn categorymanager []
  (let [cats (re-frame/subscribe [:cats])]
    (fn []
      [:div
       (into [:ul] (map #(vector :li (:text %)) @cats))])))

Notice what has happened here. The list provided by the map is folded into the [:ul] vector. At the end of it, no list in sight. Just nested vectors.

You only get warnings about missing keys when you embed a list into hiccup. Above there is no embedded list, just vectors.

Third, if your list really is dynamic...

Add a unique key to each item (unique amoung siblings). In the example given, the :text itself is a good enough key (I assume it is unique):

(defn categorymanager []
  (let [cats (re-frame/subscribe [:cats])]
    (fn []
      [:div
        [:ul  (map #(vector :li {:key (:text %)} (:text %)) @cats)]])))

That map will result in a list which is the 1st parameter to the [:ul]. When Reagent/React sees that list it will want to see keys on each item (remember lists are different to vectors in Reagent hiccup) and will print warnings to console were keys to be missing.

So we need to add a key to each item of the list. In the code above we aren't adding :key via metadata (although you can do it that way if you want), and instead we are supplying the key via the 1st parameter (of the [:li]), which normally also carries style data.

Finally - part 1 DO NOT use map-indexed as is suggested in another answer.

key should be a unique value associated with each item. Attaching some arb integer does nothing useful - well, it does get rid of the warnings in the console, but you should use the into technique above if that's all you want.

Finally - part 2 there is no difference between map and for in this context.

They both result in a list. If that list has keys then no warning. But if keys are missing, then lots of warnings. But how the list was created doesn't come into it.

So, this for version is pretty much the same as the map version. Some may prefer it:

(defn categorymanager []
  (let [cats (re-frame/subscribe [:cats])]
    (fn []
      [:div
        [:ul  (for [i @cats] [:li {:key (:text i)} (:text i)])]])))

Which can also be written using metadata like this:

(defn categorymanager []
  (let [cats (re-frame/subscribe [:cats])]
    (fn []
      [:div
        [:ul  (for [i @cats] ^{:key (:text i)}[:li  (:text i)])]])))

Finally - part 3

mapv is a problem because of this issue: https://github.com/Day8/re-frame/wiki/Using-%5Bsquare-brackets%5D-instead-of-%28parentheses%29#appendix-2

Maraud answered 12/5, 2016 at 11:59 Comment(1)
On reddit your answer was described as epic. :-)Leaf
M
3

Edit: For a much more coherent and technically correct explanation of keys and map, see Mike Thompson's answer!


Here's how I would write it:

(defn categorymanager2 []
  (let [cats (re-frame/subscribe [:cats])]
    (fn []
      [:div
       [:ul
        (map-indexed (fn [n cat] ;;; !!! See https://mcmap.net/q/843913/-how-do-i-loop-through-a-subscribed-collection-in-re-frame-and-display-the-data-as-a-list-item !!!
                       ^{:key n}
                       [:li (:text cat)])
                     @cats)]])))
(defn main-panel []
  [:div
   [categorymanager2]])

A few points:

  1. See the re-frame readme's Subscribe section, near the end, which says:

    subscriptions can only be used in Form-2 components and the subscription must be in the outer setup function and not in the inner render function. So the following is wrong (compare to the correct version above)…

    • Therefore, your component was ‘wrong’ because it didn't wrap the renderer inside an inner function. The readme has all the details, but in short, not wrapping a component renderer that depends on a subscription inside an inner function is bad because this causes the component to rerender whenever db changes—not what you want! You want the component to only rerender when the subscription changes.
  2. Edit: seriously, see Mike Thompson's answer. For whatever reason, I prefer using map to create a seq of Hiccup tags. You could use a for loop also, but the critical point is that each [:li] Hiccup vector needs a :key entry in its meta-data, which I add here by using the current category's index in the @cats vector. If you don't have a :key, React will complain in the Dev Console. Note that this key should somehow uniquely tie this element of @cats to this tag: if the cats subscription changes and gets shuffled around, the result might not be what you expect because I just used this very simple key. If you can guarantee that category names will be unique, you can just use the :test value, or the :test value, or something else. The point is, the key must be unique and must uniquely identify this element.
    • (N.B.: don't try and use mapv to make a vector of Hiccup tags—re-frame hates that. Must be a seq like what map produces.)
  3. I also included an example main-panel to emphasize that
    • parent components don't need the subscriptions that their children component need, and that
    • you should call categorymanager2 component with square-brackets instead of as a function with parens (see Using [] instead of ()).
Monaxial answered 11/5, 2016 at 19:30 Comment(1)
This answer has been very helpful. Thank you.Leaf
S
2

Here's an ul / li example:

(defn phone-component
  [phone]
  [:li
   [:span (:name @phone)]
   [:p (:snippet @phone)]])

(defn phones-component
  []
  (let [phones (re-frame/subscribe [:phones])] ; subscribe to the phones value in our db
    (fn []
      [:ul (for [phone in @phones] ^{:key phone} [phone-component phone] @phones)])))

I grabbed that code from this reframe tutorial.

Also map is preferable to for when using Reagent. There is a technical reason for this, it is just that I don't know what it is.

Stingo answered 11/5, 2016 at 16:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.