Search code examples
clojurescriptreagentre-frame

How to reset a counter in Re-frame (ClojureScript)


This must be one of those silly/complex things that everybody founds when learning a new framework. So I have this function:

(defn display-questions-list
 []
 (let [counter (atom 1)]
     [:div
      (doall (for [question @(rf/subscribe [:questions])]
            ^{:key (swap! counter inc)} [question-item (assoc question :counter @counter)])])))

The @counter atom doesn't hold any important data, it's just a "visual" counter to display the number in the list. When the page is loaded for first time, all works fine, if there are five questions the list displays (1..5), the issue is that when a question is created/edited/deleted the subscription:

 @(rf/subscribe [:questions])

is called again and then of course the list is displayed but now from 6 to 11. So I need a way to reset the @counter.


Solution

  • You should not be using an atom for this purpose. Your code should look more like this:

      (ns tst.demo.core
        (:use tupelo.test)
        (:require [tupelo.core :as t]))
    
      (defn display-questions-list
        []
        [:div
         (let [questions @(rf/subscribe [:questions])]
           (doall (for [[idx question] (t/indexed questions)]
                    ^{:key idx}
                    [question-item (assoc question :counter idx) ])))])
    

    The tupelo.core/indexed function from the Tupelo library simply prepends a zero-based index value to each item in the collection:

    (t/indexed [:a :b :c :d :e]) =>   
    
        ([0 :a] 
         [1 :b] 
         [2 :c] 
         [3 :d] 
         [4 :e])
    

    The source code is pretty simple:

    (defn zip-lazy
      "Usage:  (zip-lazy coll1 coll2 ...)
    
          (zip-lazy xs ys zs) => [ [x0 y0 z0]
                                   [x1 y1 z1]
                                   [x2 y2 z2]
                                   ... ]
    
      Returns a lazy result. Will truncate to the length of the shortest collection.
      A convenience wrapper for `(map vector coll1 coll2 ...)`.  "
      [& colls]  
      (assert #(every? sequential? colls))
      (apply map vector colls))
    
    (defn indexed
      "Given one or more collections, returns a sequence of indexed tuples from the collections:
          (indexed xs ys zs) -> [ [0 x0 y0 z0]
                                  [1 x1 y1 z1]
                                  [2 x2 y2 z2]
                                  ... ] "
      [& colls]
      (apply zip-lazy (range) colls))
    

    Update

    Actually, the main goal of the :key metadata is to provide a stable ID value for each item in the list. Since the items may be in different orders, using the list index value is actually a React antipattern. Using a unique ID either from within the data element (i.e. a user id, etc) or just the hashcode provides a unique reference value. So, in practice your code would be better written as this:

    (defn display-questions-list
      []
      [:div
       (doall (for [question @(rf/subscribe [:questions])]
                ^{:key (hash question)}
                [question-item (assoc question :counter idx)]))])
    

    Some hashcode samples:

    (hash 1)                   =>  1392991556
    (hash :a)                  => -2123407586
    (hash {:a 1, :b [2 3 4]})  =>   383153859