tonsky / rum

Simple, decomplected, isomorphic HTML UI library for Clojure and ClojureScript
Eclipse Public License 1.0
1.79k stars 125 forks source link

Question about rendering performance when updating state asynchronously #236

Closed Jarzka closed 3 years ago

Jarzka commented 3 years ago

I have the following example component. It depends on three different atoms, and it contains a button which updates all atom values asynchronously.

(def value1 (atom 1))
(def value2 (atom 10))
(def value3 (atom 100))

(rum/defc render-example < rum/reactive []
  (println "Rendering example")
  [:<>
    [:div (str "Values: " (rum/react value1) ", " (rum/react value2) " and " (rum/react value3))]
    [:button {:on-click (fn []
                         (go
                           (swap! value1 inc)
                           (swap! value2 inc)
                           (swap! value3 inc)))}
      "Increment all (async)"]])

Now when I press the button, the component is rendered three different times ("Rendering example" is printed three times). Why is that? I would have guessed that when I press the button, all the atoms are updated and only after the asynchronously event has been completed, the component would render itself once.

It works if I do the update synchronously (without go block). In that case the component is rendered once. But let's say I absolutely need to do the update asynchronously. Why is the component updating three times in this case?

Azzurite commented 3 years ago

And whatever the answer for "why is it updating 3 times" is, I'd personally also like to know how to avoid this :)

roman01la commented 3 years ago

This is a React question. React batches updates scheduled synchronously in React-controlled phases, such as event handling in this case. When called outside of such controlled phases, such as asynchronously in an event handler in this case, React is not able to batch scheduled updates, thus component update is triggered for every scheduled update. For those cases one can use React's batching API (batchedUpdates), to explicitly batch updates that are out of React's control

Jarzka commented 3 years ago

That's interesting, because the same example in Reagent only renders the component once, even if I update the atoms asynchronously:

(def value1 (r/atom 1))
(def value2 (r/atom 10))
(def value3 (r/atom 100))

(defn render-example []
          (println "Rendering example")
          [:<>
           [:div (str "Values: " @value1 ", " @value2 " and " @value3)]
           [:button {:on-click (fn []
                                 (go
                                   (swap! value1 inc)
                                   (swap! value2 inc)
                                   (swap! value3 inc)))}
            "Increment all (async)"]])

I wonder if Reagent has done some optimisations when multiple Reagent atoms are being updated, so that the component is not rendered between the updates.

roman01la commented 3 years ago

Yes, Reagent is using its own scheduling and processing queue, Rum used to have that as well, but that was changed to be closer to React, be compatible with concurrent mode and avoid issue with input fields

Jarzka commented 3 years ago

Okay, good to know. Thanks for the answer.

Azzurite commented 3 years ago

The thing though is, how is it possible to batch your own updates when using the reactive mixin? Wouldn't the mixin need to do that? It'd be a bit weird if in fact the solution is to program your own reactive mixin.

roman01la commented 3 years ago

batching happens on caller's side

(react-dom/unstable_batchedUpdates
  (fn []
    (swap! value1 inc)
    (swap! value2 inc)))
Azzurite commented 3 years ago

I see, thank you very much :) I didn't understand how that feature worked when reading about it.

Azzurite commented 3 years ago

@Jarzka is this closeable?