Fork me on GitHub
#reagent
<
2019-06-19
>
Matt Phillips00:06:29

Hello all. We have a reagent template page that shows at toolbar, content and optionally a popover dialog.

(defn page []
  [:div
    (when @show-popover
      [popover ...])
    [toolbar ...]
    [content ...]])
When we (reset! show-popover true) to show the dialog we’ve found that the VDOM for toolbar and content get fully re-built even though they’re not dependent on it. If we move the (when @show-popover ...) last, they don’t. This is quite surprising, and I’m wondering if it’s accidental or if I’m missing something important about reagent/react?

aisamu01:06:34

It looks like there's something else going over there!

aisamu01:06:27

This shows that the re-renders are targeted even at the hiccup level (pre-VDOM):

(def state (atom nil))

(def output (atom []))

(defn subcomponent [prop]
  (swap! output conj prop)
  [:span prop])

(defn root []
  (swap! output conj "root")
  [:div
   [:button {:on-click #(swap! state not)} "toggle"] 
   (when @state
     [subcomponent "toggle"])
   [subcomponent "fixed"]])

(defn view []
  (swap! output conj "view")
  [:div
    [:div [:p (prn-str @state)]] 
    [:div [:p (prn-str @output)]]]) 

[:div
 [root]
 [:hr]
 [view]]
(To be pasted on https://escherize.com/cljsfiddle/)

aisamu01:06:17

If you're on mobile, output contains: • ["root" "fixed" "view"] on mount • ["root" "fixed" "view" "root" "toggle" "view"] after one click • ["root" "fixed" "view" "root" "toggle" "view" "root" "view"] after two clicks

Matt Phillips02:06:07

Yes, actually I probably misspoke when I said VDOM. This is at the reagent hiccup->VDOM level

Matt Phillips02:06:49

It seems that perhaps reagent re-renders all the components that rendered after a deref

lilactown05:06:17

in general, anytime a component is rendered, all of its children will be as well

lilactown05:06:37

this doesn’t necessarily mean that it wil be committed to the DOM, but it has to build the tree in order to diff it

lilactown05:06:46

I know that in React, you can actually short-circuit this by returning the exact same React elements as the last render. I think I remember it works the same in Reagent (by returning the same hiccup vector), but I can’t be sure

lilactown05:06:01

and by same I mean: referentially the same

lilactown05:06:29

if that’s the case with reagent, this is still tricky because our renders can have side effects (dereferences) so we can’t just blindly memoize our components

Matt Phillips05:06:48

but the interesting thing here is all the children aren’t being re-rendered in the case where the affected one is last (these are siblings)

felipebarros06:06:40

If you extract the when to a separate component, it will not trigger re-rendering of the siblings inside page, but I couldn't mimic the behavior you described. I have the following here:

(defn page []
  (.log js/console "page")
  [:div
   [:div "toolbar"]
   [:div "content"]
   (when @show-popover
     (.log js/console "popover in")
     [:div "popover"])])
And presuming show-popover is false and I (reset! show-popover true), both page and popover in are printed out to the console, independent of the position of the when statement. I believe this is the intended behavior as it would be pretty weird otherwise. How are you testing the re-rendering?
(defn popover []
  (when @show-popover
    (.log js/console "show-popover outside")
    [:div "popover"]))

(defn page []
  (.log js/console "page")
  [:div
   [popover]
   [:div "toolbar"]
   [:div "content"]])

Matt Phillips06:06:52

we could see the performance hit, and added printlns into the components on the page. tried a bunch of things and stumbled on changing the order

Matt Phillips06:06:48

so very sure we saw what I’m describing. will look at the actual code again to make sure my example is accurate

felipebarros06:06:12

Now I'm curious as well 🙂

aisamu06:06:17

> It seems that perhaps reagent re-renders all the components that rendered after a deref I'm not sure I understand this statement. At least on the example I've posted, fixed is only rendered once even though both the parent and sibling have re-rendered. Just tested that this also happens regardless of its position!

Matt Phillips06:06:14

@U1UQEM078 I’m not sure what’s going on here then. The actual code we’re using is of course more complex, but the updated example I posted captures pretty much all of its structure, and the order definitely makes a difference

😕 4
Matt Phillips06:06:05

I think I’ve probably had my question answered: the way we were doing it should have caused a re-render of all regardless of order. the fact that order matters may be some kind of bug or implementation detail/optimisation

felipebarros06:06:04

If you end up figuring out what happened please share.

felipebarros06:06:40

If you extract the when to a separate component, it will not trigger re-rendering of the siblings inside page, but I couldn't mimic the behavior you described. I have the following here:

(defn page []
  (.log js/console "page")
  [:div
   [:div "toolbar"]
   [:div "content"]
   (when @show-popover
     (.log js/console "popover in")
     [:div "popover"])])
And presuming show-popover is false and I (reset! show-popover true), both page and popover in are printed out to the console, independent of the position of the when statement. I believe this is the intended behavior as it would be pretty weird otherwise. How are you testing the re-rendering?
(defn popover []
  (when @show-popover
    (.log js/console "show-popover outside")
    [:div "popover"]))

(defn page []
  (.log js/console "page")
  [:div
   [popover]
   [:div "toolbar"]
   [:div "content"]])

Matt Phillips06:06:56

@anantpaatra I don’t think we tried moving the when to a sibling component

Matt Phillips06:06:44

(trying it now)

felipebarros06:06:56

Dereferencing will trigger a re-render of the entire component. Normally the impact in performance isn't big (in my little experience), but that's exactly the case for moving something to a subcomponent.

felipebarros06:06:54

In the case of a little component, that is. if it is something more complex the impact in performance will be visible of course.

Matt Phillips06:06:11

moving the deref to another component fixes it, which makes total sense. don’t know how we missed that

Matt Phillips06:06:23

but def seeing the effect of ordering still

Matt Phillips06:06:05

I don’t know if it makes any difference, but our example is more like:

Matt Phillips06:06:07

(defn page []
  (let [v @some-other-ratom]
    [:div
     [toolbar v ...]
     [content v ...]
     (when @show-popover
       [popover])]))

Matt Phillips06:06:37

v in this case isn’t being touched, but is deref’ed

Matt Phillips06:06:14

it’s sounding like it would be worth taking this to a github issue

felipebarros06:06:44

Hmm, just tried that example and I get the same result. The ordering doesn't change what happens. 😕 In my test @some-other-ratom is just a simple map that I prn-ln in the toolbar and content component calls though, so maybe there is something else going on.

Matt Phillips06:06:07

@anantpaatra weird. thanks for testing it out for me :thumbsup:

felipebarros06:06:24

No problem 🙂 All's well that ends well. haha

Ahmed Hassan09:06:11

What is difference between r/as-component and r/reactify-component?

tavistock11:06:28

if a component takes a component you would use reactify-component if it takes dom nodes you would use as-component

tavistock11:06:47

an example of the former is a Higher order component that takes a component and an example of the latter is if a component has a title prop that you can hand dom nodes to for more control in how its rendered