Skip to content

When do components update?

Mike Thompson edited this page Sep 5, 2015 · 44 revisions

In this intermediate Reagent tutorial, we answer the question: when do Components re-render and why?

Components Are Reactive

Reagent Components are "reactive" in the following way:

  • each Component has a render function
  • this render function turns input data into hiccup (HTML)
  • render functions are rerun when their input data changes, producing new hiccup
  • that new hiccup is "interpreted" by Reagent and ultimately results in new HTML

It is this whole re-running the renderer function thing that makes a Component reactive. It "reacts" to changes in its "inputs", producing a new output.

This page is about understanding how and why these reactions happen.

Reactive To What?

We start by looking at the inputs to the process. What things, when they change value, trigger a re-run of a Component's renderer?

Short answer is that there's two kinds of input data:

  • props
  • ratoms

As we'll soon see, these two kinds of input are not quite equal. There are differences in the way they trigger.

1. Props

The first of these inputs is called props.

Consider this example Component:

(defn greet
  [name]          ;; name is a string            
  [:div "Hello " name])

name is a prop (short for property). In this example, it is a string value. In our clojurescript/Reagent world, it takes the form of a parameter to the Component renderer, greet.

Each time the value of name changes over time, greet will rerender.

Wait, what? How exactly can the value of name change over time - isn't it just a parameter? Don't parameters only ever get one value, when the function is called?

Well, you'll remember from previous tutorials that greet is going to be "promoted" to be the render function of a Component. As a Component renderer, it will get called at least once, but probably many, many times. So there will be the opportunity for name to have a different value each time greet is called and, in that sense, it is a value which can change over time.

To understand further, imagine we had a parent Component, which uses greet:

(defn greet-family
  [] 
  [:div 
    [greet "Dad"]
    [greet (str "Bro-" (rand-int 10))]])

When Reagent interprets the hiccup returned by greet-family, it will create 3 further components:

  • a :div component, with two greet children
  • the 1st greet child will always be given the name "Dad". Always the same prop
  • the 2nd greet child will likely have a different value for name each time that greet-family renders. Perhaps "Bro-1" one time and "Bro-5" the next. Only 1 time in a 10 will it be the same as last time.

After a Component's renderer runs and produces hiccup, Reagent interprets it. When it processes the output of greet-family, it will check to see if these 3 rerendered Components themselves need rerendering. The test Reagent uses is a simple one: for each Component, are the newly supplied props different to those supplied in the last render. Have they "changed"?

If the props are different, then that Component's render will be called to create new hiccup. But if the props to that Component are the same as last time, then no need to rerender it.

Obviously, the [greet "Dad"] component is rendered by greet-family the same way each time, and will get the same props every time and, so, it will not need re-rendering. It will render once, at the beginning, but never again, no matter how many times its parent greet-family is rerendered.

On the other hand, [greet (str "Bro-" (rand-int 10))] will often render a different name prop. So, if greet-family rerenders, then that child component will often re-render too ... although about 1 time in 10 the prop this time will be the same as last time, and Reagent will determine that it doesn't need to be rerendered.

Which means we can now answer the question posed above - how can the value of name change over time for a given greet component? Answer: when the parent Component re-renders, and supplies a new value as the prop.

props flow from the parent. A Component can't get new props unless its parent rerenders.

2. Ratoms

Let's now discuss the 2nd form of input data to a Component.

This example is a bit contrived, but bear with me ...

(def name  (reagent.ratom/atom "Bear"))

(defn ask-for-forgiveness
  []           ;; <--- no props     
  [:div "Please " @name " with me"])   ;; notice that @

We can see that ask-for-forgiveness will return the hiccup [:div "Please " "Bear" " with me"]

Well, initially anyway, because initially name contains the string value "Bear".

Data is flowing into the render function via this name ratom. Reagent will detect that this renderer has a ratom input, and it will watch that ratom for changes.

If I suddenly got all scientific, and did this (reset! name "Ursidae"), Reagent would detect the change in name, and it would re-run any Component renderer which is dependent upon it. That means ask-for-forgiveness is re-run, producing the new hiccup [:div "Please " "Ursidae" " with me"].

Just so we're clear: a "data input" changes (the value in a ratom) and, then, the renderer is rerun to produce new hiccup. The Component is reactive WRT the ratoms it derefs.

A Combination

So that was the basics.

Let's now look at how these things can combine. We're going to consider a case involving two child components, and a parent.

Child Component 1:

(defn greet-number
  "I say hello to an integer"
  [num]                             ;; an integer
  [:div (str "Hello #" num)])       ;; [:div "Hello #1"]

Child component 2:

(defn more-button
  "I'm a button labelled 'More' which increments counter when clicked"
  [counter]                             ;; a ratom
  [:div  {:class "button-class"
 	     :on-click  #(swap! counter inc)}   ;; increment the int value in counter
	     "More"])      

And, finally, a Form-2 parent Component which uses these two child components:

(defn parent
  [] 
  (let [counter  (reagent.ratom/atom 1)]    ;; the render closes over this state
    (fn  parent-renderer 
      []
      [:div 
        [more-button counter]            ;; no @ on counter
        [greet-number @counter]])))      ;; notice the @. The prop is an int

With this setup, answer this question: what rerendering happens each time the more-button gets clicked and counter gets incremented?

Don't read on. Test yourself. Spend 30 seconds working it out.

Answer:

  1. Reagent will notice that counter has changed and that is an input ratom to parent-renderer, and it will rerun that renderer.
  2. Reagent will interpret the hiccup returned by parent-renderer, and it will determine that a new (integer) prop has been supplied in the [greet-number @counter] Component, and it will then rerender that component too.

Wait. Is that it? Why doesn't the [more-button counter] component rerender too? After all, its prop counter has changed???

No, I promise it won't rerender. But why not? The answer is a bit subtle.

You see, counter itself hasn't changed. It is still the same ratom it was before. The value in counter has been incremented, but counter itself is still the same ratom. So from Reagent's point of view [more-button counter] involves the same prop as "last time" and it concludes that there's no need for a rerender of that component.

Had more-button dereferenced the counter ratom THEN the change in counter should have triggered a rerender of more-button. But if you look at more-button you'll see no @counter. There is no dereference.

If you truly understand this example, then you've gone a long way to officially getting it.

Different

Although they are both ways to trigger a reaction, the two kinds of inputs have different properties:

  1. the definition of "changed" applied
  2. treatment of lifecycle functions

Changed?

Through this page, I've been saying a renderer will be re-run when an input value "changes".

But I've been carefully avoiding any definition of "changed". You see there's at least two definitions: = and identical?

(def x1  {:a 42  :b 45})
(def x2  {:a 42  :b 45})

(= x1 x2)
;; =>  true

(identical? x1 x2)
;; => false

For props, = is used to determine if new values have changed from the old.

For ratoms, identical? is used (on the value in the ratom) to determine if the new value has changed from the old.

So, it is only when values are deemed to have "changed", that a re-run is triggered, but there is two definitions of changed.

The identical? version is very fast. It is just a single reference check.

The = version is more accurate, more intuitive, but potentially more expensive. Although, as I'm writing this I notice that = uses identical? when it can.

Efficient Re-renders

Its only via rerenders that a UI will change. So re-rendering is pretty essential.

On the other hand, unnecessary re-rendering should be avoided. In the worst case, it could lead to performance problems. By unnecessary rendering, I mean rerenders which result in unchanged HTML. That's a whole lot of work for no reason.

So this notion of "changed" is pretty important. It controls if we are doing unnecessary, performance-sapping rerendering work.

Lifecycle Functions

When props change, the entire underlying React machinery is engaged. React Components can have lifecycle methods like component-did-update and these functions will get called, just as they would if you were dealing with a React Component.

But ... when the rerender is re-run because an input ratom changed, Lifecycle functions are not run. So, for example, component-did-update will not be called on the Component.

Careful of this one. It trips people up.