Search code examples
clojureclojurescriptreagentre-frame

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


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


Solution

  • 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