How to add data-binding delays in reagent?

This is with raw Reagent. My dataset produces a couple thousand elements on the screen (data visualization), and is tied to a text input that updates an atom which filters the results. The result with my data-size is that the browser lags up to a second on each keystroke while it is filtering all the results for each keystroke; I need this to be smooth. Conceptually a better solution would be to wait a second after the last change before making any changes to the data; however, given the naive data-binding of an input field to a reagent atom, this is not quite trivial. Has anyone solved this? Is there already a reagent function for delaying an update?

1 Like

Have you considered using the on-blur handler instead of on-change? That could be a quick fix for your issue while you search for the proper solution.

One solution I can think of is using setTimeout in your event handler, storing the return value of the setTimeout invocation for later cancelling. Pseudo-Clojure coming up:

(let [previous-timeout (atom nil)]
(defn my-handler

(when @previous-timeout
(Window/clearTimeout @previous-timeout))
(let [new-timeout (Window/setTimeout #(swap ratom some-fn) 300)]
(reset previous-timeout new-timeout)))

Sorry for not formatting this properly, my phone’s keyboard won’t let me. Also, this is entirely untested. :slightly_smiling_face:

1 Like

Thanks for the response! the onBlur idea is a good one, although I think it still isn’t the user experience we’d want. I didn’t know about Window.setTimeout, which might be gone now, but led me to its “successor”: WindowOrWorkerGlobalScope.setTimeout(). Thanks for the lead!

1 Like

Yes, that was actually the one I was thinking of. :grinning: I just couldn’t remember whether it was in global or on Window.

This is a problem that the highly anticipated React featured called Concurrent Mode is supposed to help with.

In reagent, the best way I’ve found is to debounce the mutation of the atom until the user has stopped typing for some amount of time. The Google Closure library comes with a built-in debouncer that you can use: https://www.martinklepsch.org/posts/simple-debouncing-in-clojurescript.html

This does mean that you will need to switch the input from being controlled (aka the :value is provided by dereferencing the atom) to uncontrolled (let the DOM track the state). For many cases this is fine; if not, then you need to store and update the filter input state separately from the filtered results and not debounce updates to the filter input state.

2 Likes

I’d go for the setTimeout solution together with component-local data:

  • store the state of the input field in a local atom
  • on every on-change event, register a handler with setTimeout to update the global atom.

You’d need to de-register the old handler in on-change as well, and you’d also need to de-register it on correctly. I’m not sure if it’s enough to do that in the on-blur, or if you’d need to use the component-will-unmount life-cycle fn. I think I’d go for the latter. You can store the registered handler id in the local atom as well.

1 Like