Endless changeloop for sliders
expez opened this issue · comments
Support Question
I'm sure I'm doing something wrong, but I can't figure it out. The sliders I create get into an endless change loop if I make changes too fast. It seems like when the new value is put into the view, that triggers the handler again. How can I avoid this?
Here's what I do:
(defn- my-slider-value-changed-handler [my-app-state event-data]
(swap! my-app-state assoc :my-slider-value (:fn-fx.listen/new event-data)))
(defui MySliderExample
(render [this {:keys [my-slider-value] :as my-app-state}]
[(ui/slider
:value my-slider-value
:listen/value {:event ::slider-value-changed})]))
FWIW, I tried rounding these values to lower the number of swaps on the atom, but that didn't have any effect.
Hi,
Can you provide a link to full code ? (github repo maybe).
I will try to see if I can reproduce this on my system.
Thanks,
Anuj.
Can you provide a link to full code ? (github repo maybe).
Just run -main
in the REPL and drag the slider around. It'll instantly take on a life of its own in an endless change loop.
Here's the entire thing, in case my mistake is obvious through inspection alone:
(ns test.core
(:gen-class
:extends javafx.application.Application)
(:require [fn-fx.controls :as ui]
[fn-fx.diff :refer [defui render]]
[fn-fx.fx-dom :as dom]))
(defui MyApp
(render [this {:keys [my-value] :as data-state}]
(ui/grid-pane
:alignment :center
:hgap 10
:vgap 10
:padding (ui/insets
:bottom 25
:left 25
:right 25
:top 25)
:children [(ui/slider
:id :foo
:min 0
:max 10
:value my-value
:block-increment 1
:listen/value {:event ::change-my-value}
:grid-pane/column-index 0
:grid-pane/row-index 0)])))
(defui Stage
(render [this data-state]
(ui/stage
:title "Test"
:shown true
:scene (ui/scene
:root (my-app data-state)))))
(defn -main []
(let [data-state (atom {:my-value 5})
handler-fn (fn [{:keys [event] :as all-data}]
(case event
::change-my-value
(swap! data-state assoc :my-value (:fn-fx.listen/new all-data))
(println "Unknown UI event" event all-data)))
ui-state (agent (dom/app (stage @data-state) handler-fn))]
(add-watch data-state :ui (fn [_ _ old-state new-state]
(send ui-state
(fn [old-ui]
(dom/update-app old-ui (stage new-state))))))
data-state))
can you try with the should-update? function as in the code below for MyApp.
I don't see the weird behaviour after this change.
(defui MyApp
(should-update? [this old-props new-props] false)
(render [this {:keys [my-value] :as data-state}]
(ui/grid-pane
:alignment :center
:hgap 10
:vgap 10
:padding (ui/insets
:bottom 25
:left 25
:right 25
:top 25)
:children [(ui/slider
:id :foo
:min 0
:max 10
:value my-value
:block-increment 1
:listen/value {:event ::change-my-value}
:grid-pane/column-index 0
:grid-pane/row-index 0)])))
Thank you, that solves it!
What does should-update?
do? I've looked around a little now and I can't find any other way to answer that question except digging into the code.
The "defui" macro has a default implementation of should-update? that diffs the old and new state for any component and then decides whether to re-render the component.
By giving our own function that always returns false we make fn-fx never try to re-render MyApp
Thanks, @anuj-seth! I've clearly misunderstood the data flow here. I'll elaborate in case anyone else runs into this problem.
I thought it was:
- Slider event is triggered
- App state is changed
- View is updated to reflect the new app state (slider actually moves)
Instead it should be this:
- Slider moves AND slider event is triggered
- App state changes
- View ignores changes to app state because the slider has already moved.