dcc.Dropdown pause before filtering 'options' with callback

We use the dcc.Dropdown component like a search bar in our app, with multi enabled.

The search_val that a user types is taken as input to a callback that filters the ‘options’ available to the Dropdown component.

i.e. if we have a list of countries, typing 'United" would filter out countries that don’t include ‘United’ in their name, so the ‘options’ in the dropdown might include “United States, United Kingdom, United Arab Emirates…”.

One downside is that the callback that filters the possible options fires for every character entered into the Dropdown field, so in a deployed app there are 6 callbacks fired by typing “United” that filter the options each time.

I’m wondering if anyone has any ideas of how I could pause the callback request until the user has stopped typing, so that in the previous example the user could type “United” as a whole before the callback filtering the options for the Dropdown is called, and when it is called the input of the search_val is “United”

Any help with this would be appreciated- this problem is slowing down my application for all users whenever there’s a dropdown search. (The callbacks are very fast themselves but the overhead of filtering the options takes long enough for each character that it’s noticeable). Thanks!

1 Like

Hello @jkunstle,

You are looking for debounce=True, this will only perform actions when the element is blurred or a selection has been completely defined. :slight_smile:

Thank you so much for responding!

That sounds like exactly what I’m looking for but it isn’t in the docs for the component, and it doesn’t work in practice.

TypeError: The `dcc.Dropdown` component (version 2.7.0) with the ID "<my_id>" received an unexpected keyword argument: `debounce`
Allowed arguments: className, clearable, disabled, id, loading_state, maxHeight, multi, optionHeight, options, persisted_props, persistence, persistence_type, placeholder, search_value, searchable, style, value

Any other possible solutions in mind?

Hmm, ok.

Is there a way that you can provide an MRE for this?

Yeah sure, I’ll get something working and post later.

1 Like
from dash import Dash, dcc, html, Input, Output, State, callback, no_update

app = Dash(__name__)

city_selections = [
    "New York City",
    "Montreal",
    "San Francisco",
    "Denver",
    "Jackson Hole",
    "Seattle",
]

app.layout = html.Div(
    [
        dcc.Dropdown(
            id="dropdown",
            options=["Montreal", "San Francisco"],
            value=["Montreal", "San Francisco"],
            multi=True,
        )
    ]
)


@callback(
    Output("dropdown", "options"),
    Input("dropdown", "search_value"),
    State("dropdown", "value"),
)
def options_one(user_input, selections):
    """
    Callback fires every time the 'search_value' changes
    in the dropdown menu, which is whenever the user adds
    or deletes a character from the search field.
    """

    # nothing currently selected in the searchbar
    if not selections:
        selections = []

    # user has deleted any typed search term
    if not user_input:
        return no_update

    # patter match strings in selectable options against the user's input
    opts = [city for city in city_selections if user_input.lower() in city.lower()]

    print(f"User input: {user_input}")
    print(f"Possible options: {opts}")

    """
        Normally, the length of the 'city_selections' value in our app's context
        is around 15,000 entries long, which is too long for the searchbar,
        so we return a trimmed options list like this, limiting the number of options
        to a manageable 250 + selections values:

        if(len(opts) > 250):
            return opts[:250] + selections
        else:
            return opts + selections
    """

    # concat previous selections onto options, otherwise they won't
    # be kept as selected values.
    return opts + selections


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

This MRE implements the way we handle option filtering in our app.
What would be nice is if the parameter ‘debounce’ was available like @jinnyzor mentioned
so that the callback would only fire when the user stopped typing for a reasonable amount of time.

@jkunstle,

Ok, you are using search_value, this is a funky way to go about it.

I see it’s because your list is massive.

Could you possibly use an input and then a table and the user selects the options from there? XD

Maybe you could try something like this from dmc, it has debounce, just not sure if that carries over to searchValue:

Okay I’ll give that a try! Thanks for the suggestion. That’s a good start- just knowing that ‘debounce’ is probably the right term to look for is useful.

1 Like

Interesting findings: with the following code the debounce property doesn’t seem to do anything:

from dash import Dash, dcc, html, Input, Output, State, callback, no_update
import dash_mantine_components as dmc

app = Dash(__name__)

city_selections = [
    "New York City",
    "Montreal",
    "San Francisco",
    "Denver",
    "Jackson Hole",
    "Seattle",
]

app.layout = html.Div(
    [
        dmc.MultiSelect(
            label="Select a City:",
            id="dropdown",
            data=["Montreal", "San Francisco"],
            value=["Montreal", "San Francisco"],
            searchable=True,
            clearable=True,
            debounce=50000,
        )
    ]
)


@callback(
    Output("dropdown", "data"),
    Input("dropdown", "searchValue"),
    State("dropdown", "value"),
)
def options_one(user_input, selections):
    """
    Callback fires every time the 'search_value' changes
    in the dropdown menu, which is whenever the user adds
    or deletes a character from the search field.
    """

    # nothing currently selected in the searchbar
    if not selections:
        selections = []

    # user has deleted any typed search term
    if not user_input:
        return no_update

    # patter match strings in selectable options against the user's input
    opts = [city for city in city_selections if user_input.lower() in city.lower()]

    print(f"User input: {user_input}")
    print(f"Possible options: {opts}")

    """
        Normally, the length of the 'city_selections' value in our app's context
        is around 15,000 entries long, which is too long for the searchbar,
        so we return a trimmed options list like this, limiting the number of options
        to a manageable 250 + selections values:

        if(len(opts) > 250):
            return opts[:250] + selections
        else:
            return opts + selections
    """

    # concat previous selections onto options, otherwise they won't
    # be kept as selected values.
    return opts + selections


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

If I let the component handle its own search (just not registering a callback to the ‘searchValue’ field of the component) the debounce doesn’t seem to apply to the user’s input either- the filtered results are returned immediately. Maybe it’s not implemented? There aren’t any indications that it actively applies to the MultiSelect component in the codebase:

Is it possible for you to generate a workaround like this?

@jinnyzor I didn’t pursue this further but yours might be a good solution- the debounce on Input components would likely achieve what I’m looking for.

I am aware this is old, but since I found this very useful, maybe someone else finds this in the future.

Inspired by the solution above, I implemented a simple workaround by first checking the length of the searchvalue.

So this implements a kind of debouncing where you can specify how many letters minimum someone has to type before they get results back. so in my case for 3 letters or more, I start filtering and sending back options, for 1 and 2 letters, I just return empty (or what is in the value field already).

if searchval is not None and len(searchval)>2:  # filter for none first, as this fires on launch/empty searchbar
    options = my_filter_options_func(searchval)
    return options.extend(value)
else:
    return value
1 Like

@marcmuc Thanks so much for extending the conversation, this might be a really good fit for some use-cases. I’m glad this conversation was helpful for you!