Welcome to the third leg of the Reagent deep dive tour.
In part 2 we got into the thick of the component lifecycle. Today we move on to nuances surrounding the representation of sequences. This will be quite a change of pace as it is a somewhat less tangible topic. We'll need to use our imaginations a little.
You can edit the examples in this page (thanks to KLIPSE). Simply modify the code in the example boxes to complete the exercises.
Sequences
Seven. Apparently this is the minimum number of times a person has to try to do something before said person actually succeeds in doing it. -- Rusty BentleyIn part 1 it was noted that
[[:div] [:div]]
is not a valid Reagent component. However [:div [[:div] [:div]]]
is a valid component, and has a special meaning. Reagent forms do allow a sequence as a child of a tag. A sequence is interpreted as a React array, which is interpreted to mean a collection of elements which may get swapped around.So we can write a component as:
Example U: A sequence of circles
[:svg {:width 200 :height 200}
(for [i (range 30)]
[:circle
{:r (* (inc i) 5)
:cx 100
:cy 100
:fill "none"
:stroke (str "rgb(0, " (* i 10) "," (* (- 30 i) 10) ")")}])]
Instead of using into like we have been thus far:
(into [:svg] (for ...))
Note that the former results in something like:
[:svg [[:circle] [:circle] ...]]
While the later results in something like:
[:svg [:circle] [:circle] ...]
What difference does it make? Not much that we can notice at first glance. We'll only notice a difference if we open the developer console. Example U causes React to print warnings in the developer console:
Warning: Every element in a seq should have a unique :key
What the heck is React complaining about?
Well, imagine for a moment that you had written the following list:
- Princess torte.
- Black forest gateau.
- Apple pie.
Imagine for a moment that you emailed me the initial list and I put it into a spreadsheet. Then you sent me the final list. I could probably figure out to insert ice cream at the top is the most efficient change. So I make the update. But what if you emailed me a list of about 100 things, and then later emailed me the same list but with 20 items inserted, moved, deleted, or changed? It would be really hard for me to figure out the minimal updates required to update my spreadsheet. So instead I would copy the entire list over the top of my spreadsheet.
There is however another solution! When you sent me the list you could have assigned each logical item an id number. That way when I got the second list, I could quickly identify the minimal updates required. I could walk through the new list, looking only at the id number. If the number was in the existing list and at the same position I could check that the item contents haven't changed and then move on. If the existing list had the id number at a different position I could move it to the correct position. If the existing list didn't have the id number, I would know to insert a new row. This sounds a little tedious, but I hope it is clear that there is a mechanical process available that which would ultimately result in less change to the spreadsheet.
This minimal update scenario applies to HTML elements. React's job is to figure out the minimal changes to make to a HTML page in order to transition from the existing view state to the desired view state. If it can swap HTML elements around, that is far fewer updates than recreating them in different positions. You can give React the hint it needs by passing a unique key per element. To specify a key on an element in a sequence, use the mysterious
^{:key k}
Tip: The item key is represented in metadata attached to the item. ^
is shorthand to set the metadata of an object. You can also pass :key
as an attribute of a hiccup form.
Here is how we represent a sequence of keyed entities:
Example V: A sequence of sub-components, with identity keys
(def favorites
(reagent/atom
{"d632" {:name "Princess torte."
:order 2}
"1ae2" {:name "Black forest gateau."
:order 3}
"5117" {:name "Apple pie."
:order 4}
"42ae" {:name "Ice cream."
:order 1}}))
(defn list-by [entities sort-k]
[:ul
(for [[k v] (sort-by (comp sort-k val) @entities)]
^{:key k}
[:li (:name v)])])
(defn favorites-by-order-and-name []
[:div
[:h3 "By order"]
[list-by favorites :order]
[:h3 "By name"]
[list-by favorites :name]])
The idea in this example is that each item has a unique ID, perhaps assigned by a database, generated when the item was created. That unique ID is used as the key in the sequence.
This example is clearly contrived! These components aren't big enough or numerous enough for us to waste our precious brainpower worrying about their rendering performance. For small lists of small components, you wont notice any performance difference between the three options for representing them; as direct children of a parent element, as a keyless sequence of elements, or as a keyed sequence of elements. However this tiny example does allow us to discuss exactly what those 3 alternatives look like, and why in the broader picture we should care.
- We could have used
into
to make all the list items direct children of the unordered list. This would have resulted in no warnings and not required the key. Updating the list with new data would result in some unnecessary DOM updates. - We could have left off the
^{:key k}
and ignored the warnings in the developer console. Updating the list with new data would result in some unnecessary DOM updates. - As presented there was a natural key available, so we annotated the each list item with metadata. There are no warnings in the console and updating the list will result in fewer DOM updates.
The pattern of "I have a bunch of entities I need to render" pops up here and there in practice. That is why the distinction between expressing sequences exists. By way of illustration I shall describe one such scenario. In my Napkindo app I display a gallery of drawings. Each drawing is an entity that has a database assigned id, and various information attached to it such as the drawing title, the line paths in the drawing, and the owner. For very large collections of large elements, assigning React keys improves UI performance. And it turns out that many of the large collections of large elements we run into fit this pattern nicely.
There is another scenario where entity identity should be preserved; animating transitions of elements. Generally we don't care which elements in our DOM contain what HTML because it all looks the same once the updates are applied. But when rendering transitions, it becomes obvious which elements are linked to which logical entities. Visual identity must follow logical identity. I'll let you ponder that.
In this highly technical diagram each box is logically bound to its contents. Keys provide the mechanism to make this binding.
Coming back to our options... now that we have pondered at length the 3 ways of expressing sequences we can happily choose whichever we prefer, realizing that it wont make any difference in most circumstances. My personal opinion is that warnings are best heeded, so I avoid keyless sequences. My rule of thumb is to key my sequences if there is a natural key available. If there is no natural key, I default to using
into
the parent tag instead. I think it best to avoid the temptation to make up a sequential key by assigning each item in the sequence an index number. Doing so complicates the code to avoid a warning by deceiving React about the identity semantics of the sequence. The semantic of a key is that it identifies a unique entity in the sequence. In short, if there is a natural key, use it. If there is no natural key, put the items into the parent tag as children.There is one final consideration when using sequences; lazy deref is not allowed.
Example W: A button that does not work due to lazy deref
(def message (reagent/atom "Everything is fine"))
(defn bad []
[:div
[:button
{:on-click
(fn [e]
(reset! message "Oh no, oh dear, oh my."))}
"Panic!"]
(for [i (range 3)]
[:h3 @message])])
This example does not work! It will produce a warning:
Warning: Reactive deref not supported in lazy seq, it should be wrapped in doall
From Reagent's perspective calling bad
does not deref message
.
Rendering [:h3 @message]
occurs later but at that point Reagent no longer knows that the parent component is bad
.
Because Reagent doesn't evaluate the lazy sequence immediately it is unaware that bad
should respond to changes in message
.
We can force evaluation of a lazy sequence that derefs by wrapping it in a
doall
,
or by using using vec
or into
to realize the sequence, and then it will work just fine.
Exercise: Pressing the panic button does nothing. Fix example V by forcing evaluation of the lazy sequence, and press the panic button.
Fortunately this somewhat confusing circumstance of deref inside a lazy sequence occurs rarely, and produces a warning with the advice on how to remedy it. Notice that we don't need to force a lazy sequence that consumes a deref, which is far more common. For example
(for [x @xs] ...)
does not need to be forced because the deref is not
inside the lazy sequence.An intuition for sequence performance
The proof is in the pudding. -- UnknownI made some pretty bold claims about the impact of keys and haven't provided a guide for exactly when performance starts to be impacted aside from some vague notion of "large". It is not a simple thing to quantify and will of course be situational, but we can gain a bit of intuition here of what a "large" sequence is. Let's play out our earlier thought experiment about a list of tasty desserts getting updated.
Example X: A large keyed sequence of delectable disposition
(def words
["ice" "cream" "chocolate" "pastry" "pudding" "raspberry" "mousse"
"vanilla" "wafer" "waffle" "cake" "torte" "gateau" "pie" "cookie"
"cupcake" "mini" "hot" "caramel" "meringue" "lemon" "marzipan" "mocha"
"strawberry" "tart" "custard" "fruit" "baklava" "jelly" "banana" "coconut"])
(defn rand-name []
(string/capitalize (string/join " " (take (+ 2 (rand-int 5)) (shuffle words)))))
(def desserts (reagent/atom ()))
(defn make-a-dessert [e]
(swap! desserts conj {:id (random-uuid)
:name (rand-name)}))
(defn make-many-desserts [e]
(dotimes [i 100]
(make-a-dessert nil)))
(defn color-for [x]
(str "#" (.toString (bit-and (hash x) 0xFFFFFF) 16)))
(defn dessert-item [{:keys [id name]}]
[:li
[:svg {:width 50 :height 50}
[:circle
{:r 20 :cx 25 :cy 25 :fill (color-for id)}]
[:rect {:x 15 :y 15 :width 20 :height 20 :fill (color-for name)}]]
[:span [:em [:strong name]]]])
(defn desserts-list []
[:ol
(for [dessert @desserts]
^{:key (:id dessert)}
[dessert-item dessert])])
(defn dessertinator []
[:div
[:button {:on-click make-a-dessert} "Invent a new dessert"]
[:button {:on-click make-many-desserts} "Invent 100 new desserts"]
[desserts-list]])
Exercise:
desserts-list
currently keys each dessert-item
.
Invent 2000 desserts by pressing the "100" button 20 times.
Then add another single dessert. Creating desserts should be fairly fast. Next delete the ^{:key (:id dessert)}
line in desserts-list
and perform the same steps. At about 2000 desserts, it takes noticeably longer to create new desserts!Tip: Every time you change the code, the desserts list is reset, so you might want to make another change to the code so that you can finish the article when you are done experimenting.
As you can see, computers are amazing and it really does take a very large sequence before performance is impacted by the lack of a key. With the keyed approach we can preserve performance with many, many items in our sequence.
Conclusion
A gene is a long sequence of coded letters, like computer information. Modern biology is becoming very much a branch of information technology. -- Richard DawkinsPhew! We made it to our third stop off.
We observed 3 different ways to express a sequence of elements and discussed how React treats them. In the 4th leg of the tour we shall not be encountering any new concepts. Instead we will be applying some of the concepts we have already encountered to build out a mini sketching application.
I hope to see you again soon for part 4, where we'll handle some more practical UI challenges.