Skip to content

Using [] instead of ()

Mike Thompson edited this page Aug 6, 2015 · 82 revisions

This is a quick tutorial around the use of () and [] in Reagent renderers.

Reagent is a terrific library. You get going fast and everything just kinda works.

But sooner or later, you'll need to understand how some of that sweet magic happens. This tutorial discusses an area that might eventually puzzle you. One day, in the peace of a shower, or on that bike ride to work, or perhaps lazing in the hammock, your eyes will flick to the left and you'll wonder to yourself "wait, how the hell has this EVER worked for me all this time?". This will be followed by doubts like "if I don't understand this, what else have I been unconsciously incompetent about?" which will be followed by "do I really understand ANYTHING?". We don't want you going through that pain, so read on.

Components

In an earlier tutorial Creating Reagent Components , we saw that the centerpiece of a Reagent/React component is a renderer function. Optionally, a Component might have other lifecycle functions, but a renderer function is central and mandatory.

We also saw that Reagent render functions turn data into hiccup: data -> hiccup

Meet greet

Here's an example Component:

(defn greet
  [name]
  [:div  "Hello " name])

No, wait, what? That's not a Component, that's a function.

Yes, indeed it is. And, right there, we have the nub of the issue.

It is only when greet is used in a particular way, that it is "promoted" to become the renderer for a Reagent Component. Unless you are paying attention, you might not even realise when that magic is happening, and not happening.

Right. So given use is king, let's look at two forms of it ...

Using Greet Via ()

If you use greet in a function call via (), it returns a vector with 3 elements:

(greet "You")
;; => [:div  "Hello " "You"]   ;; a vector of a keyword and two strings

(first (greet "You"))
;; => :div

(second (greet "You"))
;; => "Hello"

So, simply calling such function certainly doesn't magically create a Reagent Component. It just returns a vector.

Hmmm. How about we call greet within another function:

(defn greet-family-1
  [member1 member2 member3]
  [:div
    (greet member1)    ;; return value put into the vector
    (greet member2)    ;; and again 
    (greet member3)])  ;; and again

greet-family-1 returns a 4 element vector. And what's in that vector if we call it like this?

(greet-family-1 "Mum" "Dad" "Aunt Edith")

Because (greet "Mum") returns [:div "Hello " "Mum"], that's the 2nd element in the vector. And so on.

 [:div
    [:div  "Hello " "Mum"]          ;; <-- (greet "Mum") 
    [:div  "Hello " "Dad"]          ;; <-- (greet "Dad") 
    [:div  "Hello " "Aunt Edith"]]  ;; <-- (greet "Aunt Edith") 

Using Greet Via []

Let's keep greet the same, but change the way it is used:

(defn greet-family-2
  [member1 member2 member3]
  [:div
    [greet member1]      ;; not using ()
    [greet member2]     
    [greet member3]])  

Let's be crystal clear: [greet member1] is a two element vector, just like [1 2].

And, what would this call return?

(greet-family-2 "Mum" "Dad" "Aunt Edith")  

Answer:

 [:div
    [greet "Mum"]         
    [greet "Dad"]
    [greet "Aunt Edith"]]

So, greet is not called inside of greet-family-2. Instead, it is placed into a vector.

The Key Difference Between () and []

There are no references to greet in the hiccup returned by greet-family-1.

 [:div
    [:div  "Hello " "Mum"]          ;; the return value of greet put in here
    [:div  "Hello " "Dad"]          ;; and again 
    [:div  "Hello " "Aunt Edith"]]  ;; and again

On the other hand, there are references to greet in the hiccup returned by greet-family-2

 [:div
    [greet "Mum"]         
    [greet "Dad"]
    [greet "Aunt Edith"]]

The Interpretation Of Hiccup

After renderers return hiccup, Reagent interprets it.

As it is interpreting hiccup, if Reagent sees a vector where the first element is a function, for example [greet "Mum"], it interprets that function as a renderer and it builds a React component around that renderer.

Let's pause and remember that a renderer function is the key, mandatory, central part of a Component. Defaults can be supplied for the other React lifecycle functions, like component-should-update, but a renderer must be supplied.

So Reagent recognises greet as a candidate renderer function and, if it is found in the right place (1st element of a vector), Reagent will mix it with other default lifecycle functions to form a full React/Reagent Component. It gives greet a promotion.

The other elements of the vector, after greet, are interpreted as parameters to the renderer (in React terms, props).

Same DOM

But both approaches will result in the same final DOM!! So why favour one over the other?

The difference is subtle (but important): the use of [] means you get a distinct React component which will have its own React lifecycle, and can be rerendered (new VDOM) independently of its siblings. And that doesn't happen if you use ().

Next Up

We've now seen how we can use functions and [] to create Components. In the next tutorial (yet to be written), we'll understand how and when these Components update.

Appendix #1

hiccup can be created like any normal cljs data structure. You don't have to use literals.

Our version of greet-family-1 from above returns something of a 4 element vector literal:

(defn greet-family-1
  [member1 member2 member3]
  [:div
    (greet member1)    
    (greet member2)  
    (greet member3)]) 

Here's a rewrite in which the hiccup is less literal and more generated:

(defn greet-family-1b       ;; a re-write of greet-family-1
  [& members]
  (into [:div] (map greet members)))

When called with 3 parameters, the two versions return the same hiccup:

(= (greet-family-1  "Mum" "Dad" "Aunt Edith") 
   (greet-family-1b "Mum" "Dad" "Aunt Edith"))
;; => true

Appendix #2

When interpreting hiccup, Reagent regards vectors as special, and it has some demands about their 1st element.

In Reagent hiccup, the 1st element of a vector must always be something it can use to build a Component.

Reagent can use greet to build a Component, so that works. So does :div because Reagent knows what Component you mean. And there are a few other options.

But if your hiccup contains a vector like [1 2 3], then you'll get an error because Reagent can't use 1 to build a Component.

So this code has a problem:

(defn greet-v
   [v]
   (into [:div] (map greet v)))

(defn greet-family
   []
   [greet-v ["Mum" "Dad" "Aunt Edith"]])  ;; <-- error here

Notice the vector ["Mum" "Dad" "Aunt Edith"] in the hiccup. Reagent will try to build a Component using "Mum" (1st element in a vector) and, when that doesn't work, it will report an error.

Clone this wiki locally