Simple debouncer

It’s common to debounce user input and do some action (e.g. HTTP request) after it is settled. For instance, request autocompletion from server or check login is already registered. There are another more complex time-throttling Elm libraries. My new library is the most simple one. You may understand how it’s work from the first glance.

I read through your source code and found it a bit confusing. For example am I right to believe that push and pop are meant to increase and decrease time? So why are they not named addTime and removeTime or something similar?

I tried to come up with a very simple solution once before, but my solution is not as minimalistic as yours:

I like the idea of the Bounce type essentially being a counter a lot. This really sparked my interest in this topic again. But as it is right now, I feel like the tricky parts to debouncing are left a bit untouched with your package.

1 Like

Let’s suppose Alice and Bob are in the garden. Alice shoot arrows. Bob is sunbathing.While arrow is in flight it is not safe to sunbath. It doesn’t matter how long time arrow flies if Bob counts launches (push) and landings (pop). When Bounce counter is zero, there is no arrows in flight, or equivalently, flight time is elapsed for every arrow. So, there is no explicit concept of time in Bounce counter.

I’ve tried another debounce libraries and they are too complex for form validation. It’s interesting, what tricky parts do you mean?

I can’t quite adapt your analogy to filling in a form. So Bob is the server and Alice is the user? What are the arrows? Also in your Analogy, Bob shouldn’t count the arrows, instead he should count the time while no arrows are in sight (as he might start sunbathing a bit to soon and get shoot by a late arrow)

If you look into the thread that I have posted, you see the original problem specification:

The Form contains a “username”, a “password” and a “passwordRepeat” field.

  • The server should check if the username is free
  • The server should check if the two passwords are the same and if the password is contained in the list of unsave passwords

To do so the following should hold true:

  • The server will wait until the user has stopped typing within 500ms.
  • If the user starts typing again, the server request should be aborted
  • Switching between input fields should be considered
    • Switching to one input and back to the other within the 500ms should not trigger an abort Event
    • No data may be lost during switching

Yea! Bob might start safe sunbathing (or send HTTP request to server) as soon as last arrow is landed, i.e. there is 0 arrow in flight. Alice launch arrow (press key) and Bounce.delay its landing. At launch moment Bob Bounce.push, at landing moment Bounce.pop

Probably, analogy is controversial, but code is monosemantic.
You may try elm reactor on
example where request is sent to the server after 1000ms of user idle. And you may use Bounce as building block in your solution. But HTTP request cancelation is out of scope of the debouncing library.

:thinking: Okay, so in your example it seems like you are counting the key presses. And for every key-press you wait specified amount of time. So longer messages will have a longer debounce time than shorter ones.

Now I’m getting your analogy. Interesting.

I simply add a delayed message step to call my command only if the input is still the same after the delay. It also allows me to easily add some validation (not included in the example below for readability).

delayMsg : msg -> Cmd msg
delayMsg msg =
    Task.perform (always msg) (Process.sleep 1000)


-- in your update cases:

GotFieldInput fieldInput ->
    ( { model | fieldInput = fieldInput }
    , delayMsg (GotDelayedFieldInput fieldInput)
    )

GotDelayedFieldInput fieldInput ->
    if fieldInput == model.fieldInput then
        ( { model | status = Loading }, costlyCommand fieldInput )
    else
        ( model, Cmd.none )

I don’t see how it could be simpler :wink:

3 Likes

Too simple to be correct :wink:. There are some cases this solution emits redundant commands when user use backspace or copypaste.

Counter example is infinite sequence of fieldInput values "a", "ab", "a", "ab", "a", "ab" .... User is always typing but command is emmitted with 50% probability.

Sure, but

  1. the counter example will never occur in real life
  2. if more commands are triggered that should be no big deal, well hopefully for something that happens at key strokes :wink:
  1. And one moment you find yourself in deep debugging.
  2. Unreliable approach is not composable. But we may with ease make up more complex systems from reliable Bounce. For instance, wait while user is typing or moving mouse. Or show spinner while any of two HTTP requests are loading.

Interpret Bounce as synchronization primitive.

  1. As I’m simply using messages no issues with debugging thanks to The Elm Architecture.
  2. I’m using spinners in my debounced forms (see the Loading status of the simplified example), it is quite easy to add too.

Do you mean your Bounce is chainable?

It’s great that you made a debounce library. Just like you I found debounce libs to be unnecessarily complex. That’s why for simple unexceptional cases I came to use this delayMsg way. Plus it’s nice to have no dependencies :wink:

1 Like
  1. Depends on problem you solve. Elm Architecture solves event loop dispatching problem, it doesn’t solve business logic state management problems.
  2. I guess, you show one spinner for each field. And I mean, we have to show one spinner for all fields together. Or enable Send button as soon as all fields are validated on the server.

Bounce is not just chainable/andThenable (associative), it is commutative (it is just Int counter). So, messages can arrive in any order with different delays. Your solution commutative too, but it say "fieldInput is same as 1 second ago". It does not say “user is not typing”.

My library is not rocket science :laughing: just increment and decrement one can do themselves without dependency. But this pattern turns out surprisingly useful. There were earlier versions of solution where I had sent timestamp or value as you, but they were not so robust and simple.

Thanks for discussion :smiley: It helps to improve unclear lib docs motivation and add more expressive examples.

2 Likes

I hacked up a combined Debounce + Throttler library a month or so ago, which I’ll probably publish v3ery soon here.

(Not to discount your original post @Grotsev, but just in case you or anyone else here finds this approach handy.)

The advantages of the approach I’ve taken are:

  • Can be used as just a debouncer, just a throttler, or both.

  • Provides Google Docs style saving behavior, e.g. I want to save 1 second after the user stops typing, but also save at least every 6 seconds in case they continually type.

  • A single Debouncer msg in your model (for a given debounce/throttle time). No need to instantiate a separate debouncer for every thing you need debounced.

  • A very simple API, poke & cancel (excluding the standard TEA init & update)

  • Msg being fired can be changed mid-debounce/throttle, e.g. incase a validation error suddenly appears.

  • Only 125 lines of code

The downside is it’s not 100% accurate to the millisecond, which I think is fine for cases of human interaction. e.g. you may have a debounce of 1 second, but it actually debounces at 1.3 seconds. This is due to how Msg.Check works.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.