Time-Based Debouncing with Plotly Dash

It’s been a while since I’ve written a post here! Ended up shifting jobs a couple months ago and have been pretty focused on a learning a new type of role. Part of the new gig is automating the collection and display of a variety of data and I’ve had the opportunity to start using Plotly’s excellent Dash library to easily tackle just about everything.

Dash is a powerful framework that allows you to create a full-stack application while only writing Python. While Python has very popular backend solutions such as Flask and Django, the frontend always ends up being some flavor of Javascript framework – recently I’ve finally become comfortable with React. With Dash you define the frontend components using Python objects and then connect them to the backend with a callback system, cleverly placed in a decorator format to easy handle inputs, outputs, and state to call Python functions on the backend server.

One thing that I miss about using Javascript (note that you can use Javascript files in Dash – it’s a very flexible tool) is how easy it is to “debounce” a function by using a setTimeout() and clearing it each time an event fires. Something like a search bar that calls an API for results needs this so you’re not pinging the server on every keystroke event; you often want to let the user type a bit and then only perform the search after a half second or so. Unfortunately debouncing in Dash terms means the action will fire after hitting enter or leaving focus on the input, less than ideal for an autocomplete search.

This post will show a way to perform a debouncing action by using a Dash Interval component and chaining the callbacks to only fire the function after typing has stopped for a defined amount of time.

Layout

For simplicity’s sake, we’re going to make a very minimal app inside a single file to demonstrate this time-based debouncing. Create a new app.py file and define the layout like so:

app = Dash(__name__)

app.layout = html.Div([
    html.H1("Dash Debounce!"),
    dcc.Input(id="input"),
    html.P(id="status"),
    dcc.Interval(id="timer", interval=500, max_intervals=1, disabled=True)
])

if __name__ == "__main__":
    app.run_server(debug=True)

If you run this app you’ll see a basic header, input, and paragraph with zero functionality. The Interval component is key here – we’re going to chain a callback to put it in between the input and the output paragraph. I’m using 500 milliseconds as the delay – feel free to use whatever you’d like.

Callback Setup

Normally we would define a callback to fire when the input element’s value changes – like sending off a search request. Instead of the search, we’re going to “turn on” the Interval component.

Create a callback on the input, but point the output to a couple Interval values:

@callback(
    Output("timer", "n_intervals"),
    Output("timer", "disabled"),
    Input("input", "value"),
    prevent_initial_call=True
)
def start_timer(value):
    return 0, False

If you look closely to this it begins to make more sense. Every time a keystroke is hit, the Interval “starts over” by setting the n_interval count back to 0 (and ensuring the interval is active).

If you keep typing into the input field before the interval can “fire” it will never go above 0 on its n_interval count. You can probably see where this is going:

@callback(
    Output("status", "children"),
    Input("timer", "n_intervals"),
    State("input", "value"),
    prevent_initial_call=True
)
def fire_search(n_intervals, value):
    if n_intervals == 1 and value != "":
        return "searched: " + str(value)

Once there has been a long enough delay between typing events n_intervals will go to 1 and this callback will execute. In this example the “search” will fire and populate the status paragraph item showing that it was successful.

Conclusion

While this is a bit hacky it actually works out pretty well. I use a similar methodology on quite a few projects where I’m rate limited by an API or don’t want to cause excess calculations. It’s straightforward enough that I wish it were included as a parameter on the Input component out-of-the-box, but easy enough to implement that I’m not too bothered.

I guess since it’s all open source I could put something together and submit a PR – maybe when I find some more of this magical free time.

Leave a comment