Just starting out with Reagent. I have a button whose :on-click
value causes a CPU-intensive function to run; it takes a long time to return. I want to update the text of the button itself to notify the user that one might have to wait, so I define a ratom that will specify the button text, and then I reset!
the ratom while the function is running.
This works if I define the ratom outside my button component function, but fails to work if I define the ratom inside the component function via let
, or if I reset!
the ratom at the top level in the component function. That is, the button text fails to change if I uncomment the first commented line below, or uncomment the two lines for the let
. Am I doing something wrong? Is this expected behavior? What is the general rule about ratoms and DOM-updating that applies here?
(def label (reagent.core/atom "Make chart"))
(defn chart-button
[normal-label running-label]
; (reset! label normal-label) ; reset globally-defined ratom
; (let [label (reagent.core/atom normal-label)] ; use local ratom
[:button {:on-click (fn []
(reset! label running-label)
(js/setTimeout (fn []
(cpu-intensive-function)
(reset! label normal-label))
10))
}
@label] ;)
)
...
[chart-button "make chart" "running..."]
...
(Not relevant, but for clarification: I use the setTimeout
trick described here to cause the DOM to update despite the fact that the long-running function would otherwise prevent the browser from updating the DOM while the function is running.)
Your problem here is that your function chart-button
gets called every time the component needs to be (re)rendered. So for example in your local reset example someone clicks the button, and your label
is reset to running-label
. Reagent detects this change and recalls your chart-button
function to see what the newly rendered button should look like, at which point your first reset changes is back. The let versions has a similar problem.
There are a couple of ways to deal with local state in Reagent. The easiest way is to return a function from your component instead of a vector, as in this example.
(defn timer-component []
(let [seconds-elapsed (r/atom 0)]
(js/setInterval #(swap! seconds-elapsed inc) 1000)
(fn []
[:div
"Seconds Elapsed: " @seconds-elapsed])))
Basically in Reagent, your component can either be the render function, or it can be a function which returns the render function. In the above case we use a closure to set up some local state, then return a render function that uses that state. Every time seconds-elapsed
is incremented the inner function is called again and the component is rerendered.
The other way is more complicated, but might help you make more sense out of this. You can take full control of the component lifecycle by using create-class instead of using functions as your components.