Liking cljdoc? Tell your friends :D

Collections

As previously described, the core Arborist protocol only covers how elements are created, updated and destroyed. There was no mention how collections of elements are handled. The core implementation indeed doesn't cover collections of elements at all. Instead this is left to specialized implementations of the core Arborist protocol. All other implementations as such only handle a single node in the tree and they look just like any other node.

Other React-like implementations usually only have one algorithm for dealing with collections. So end users create an array of elements and the library handles dealing with them in a uniform way. As far as I can tell there is no way to teach these libraries about new algos.

In Arborist there are 2 core implementations and a couple more specialized ones for certain situations.

simple-seq

simple-seq is a very basic implementation, rendering elements in order. It makes no attempts to minimize the DOM operations in any way. This is by far the fastest method of rendering a collection of elements that very rarely change.

Say you want to get this piece of DOM from a simple ["a" "b" "c"] vector.

<ul>
  <li>a</li>
  <li>b</li>
  <li>c</li>
</ul>

With simple-seq that becomes

(<< [:ul
     (sg/simple-seq ["a" "b" "c"]
       (fn [item]
         (<< [:li item])))])

Append-only collections can also be very efficient using this but anything that changes the order or deletes items at the start or middle will end up doing a lot more DOM operation than its more optimized variant the keyed-seq.

keyed-seq

This is basically the more common key-based algorithm many other implemenations such as React use. Each element in the collection needs to supply a key and that key is used to reorder DOM elements instead of rendering over them. When Element #5 was moved to #2, the actual DOM element is just moved, instead of rendering the contents of #5 into what was previously #2.

However instead of the developer's providing elements with a key, the keyed-seq instead will just take the regular collection and a key-fn so it can construct the necessary keys itself. It also happens to save one iteration over all the elements, which saves a bit of time.

Collections such as ["a" "b" "c"] that don't have a natural key are probably better handled by simple-seq. However many collections you may end up working with might have a natural key already, so we just use them.

(def data
  [{:id 1 :text "a"}
   {:id 2 :text "b"}
   {:id 3 :text "c"}])

(<< [:ul
     (sg/keyed-seq data :id
       (fn [item]
         (<< [:li (:text item)])))])

keyed-seq here takes the additional :id arguments which in this case is the key-fn. Since keywords implement the core clojure IFn protocol, they can just be called as a function, so essentially this will extract the :id from each item in the collection and use that for keyed-seq purposes. Any function taking one argument (the collection item) is valid here. identity is fine too.

Note that the above example is misleading. data is fixed and cannot change, so even though the collection has a natural key, using simple-seq here would still be more efficient. Just imagine for a sec that data isn't actually fixed and may be changing while visible on screen.

simple-seq vs keyed-seq

It is absolutely fine to only use simple-seq and only use keyeq-seq to optimize certain places.

Whether the overhead of keyed-seq is actually worth it largely depends on how often the collection is actually modified. The more items are added, removed or reordered, the more relevant it becomes.

Reagent/React comparison

In React-based libraries such as Reagent, very commonly for or map are used to render collections.

;; grove
(<< [:ul
     (sg/simple-seq ["a" "b" "c"]
       (fn [item]
         (<< [:li item])))])

;; Reagent
[:ul
 (for [item ["a" "b" "c"]]
   [:li item])]

While this looks a little shorter syntax wise, this has a couple of problems. First we really can't have laziness here at all. All collections must be forced in the render phase. If React for example would delay forcing this collection, maybe due to concurrent mode, things will get hairy very quickly.

Secondly react will yell at you since no key is provided. So you often need to invent a key here. I have seen apps that either just use the item itself, using the index together with map-indexed, or something worse such as (random-uuid). These defeat the purpose of keys completely and nullify all they are meant to optimize. I have also seen the alternative of using into, such as

(into [:ul]
  (for [item ["a" "b" "c"]]
    [:li item]))

This is reasonable since it makes react happy. As far as it is concerned it is no longer seeing a collection, just some elements. However, given how Reagent works this is much less efficient and leads to many more iterations of the collection than necessary.

Conclusion

I think simple-seq and keyed-seq provide reasonable alternatives that still look friendly enough as to not miss for too much. An alternative macro might be useful.

The burden is on the developer to pick the best variant but using either is fine for most cases. This is fine and also leaves the door open for more specialized implementations such as virtual lists. Or maybe collections that can be sorted via drag&drop. They can also do what is best for them and can re-use the existing implementations or just to something entirely custom.

Can you improve this documentation? These fine people already did:
Raphael Martin Schindler & Thomas Heller
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close