Dash 3.1 - Allow_Optional, Dropdown OpenOnSelect, Async/Await in Callbacks, Improved Persistence

Hi everyone,
A summary of Dash version 3.1 updates can be found at the end of the post. However, I wanted to draw your attention to a few interesting new changes and features.

:backhand_index_pointing_right: Allow Optional

Thank you to @jinnyzor for contributing this new feature :raising_hands:

In some cases you may have components in your app that don’t exist when a callback runs. For example, if you have a component that is conditionally rendered. By default, Dash will throw an error if you use a non-existent component as an Input or State for a callback. You’ll see an error like: 'A nonexistent object was used...'

You can set these Input or State objects to be optional using the allow_optional parameter. With allow_optional=True, the callback will receive None for any inputs or states that use it that don’t exist.

The following code example shows a button that is conditionally rendered from one callback and used as an optional input on another callback. See complete example in the docs.

from dash import Dash, html, Output, Input, State, no_update

app = Dash(suppress_callback_exceptions=True)

app.layout = html.Div([
    html.Div([
        html.Button(id="optional-inputs-button-1", children="Button 1", className="button"),
        html.Div(id="optional-inputs-container"),
        html.Div(id="optional-inputs-output", className="output")
    ], className="container")
])

@app.callback(
    Output("optional-inputs-container", "children"),
    Input("optional-inputs-button-1", "n_clicks"),
    State("optional-inputs-container", "children"),
    prevent_initial_call=True
)
def add_second_button(_, current_children):
    if not current_children:
        return html.Button(id="optional-inputs-button-2", children="Button 2", className="button")
    return no_update

@app.callback(
    Output("optional-inputs-output", "children"),
    Input("optional-inputs-button-1", "n_clicks"),
    Input("optional-inputs-button-2", "n_clicks", allow_optional=True),
    prevent_initial_call=True
)
def display_clicks(n_clicks1, n_clicks2):
    return f"Button 1 clicks: {n_clicks1} - Button 2 clicks: {n_clicks2}"

if __name__ == '__main__':
    app.run(debug=True)

:backhand_index_pointing_right: Keep Dropdown Open on Select (closeOnSelect=False)

Thank you to 12rambau for their first time contribution :raising_hands:

With Dash 3.1 you can set closeOnSelect=False on a dropdown to keep it open after each selection. Example code below. See complete example in the docs.

from dash import Dash, dcc, html, Input, Output,callback

app = Dash()
app.layout = html.Div([
    dcc.Dropdown(['NYC', 'MTL', 'SF'], 'NYC', id='close-on-select-dropdown', multi=True, 
closeOnSelect=False),
    html.Div(id='close-on-select-output')
])


@callback(
    Output('close-on-select-output', 'children'),
    Input('close-on-select-dropdown', 'value')
)
def update_output(value):
    return f'You have selected {value}'


if __name__ == '__main__':
    app.run(debug=True)

:backhand_index_pointing_right: Using Async/Await in Callbacks

Thank you to @jinnyzor for the contribution :raising_hands:

Dash supports using async/await in callbacks. To use async/await in callbacks, pip install "dash[async]".

Using async/await is useful when making requests, communicating with databases, or when using async-first libraires such as aiofiles and tortoise-orm.

The following example demonstrates using async / await in a callback.

import time
from dash import html, Input, Output, Dash
import asyncio

app = Dash()
server = app.server

def get_sync_data(iteration):
    time.sleep(1) 
    return f"Result ({iteration}) from synchronous API call"

async def get_async_data(iteration):
    await asyncio.sleep(1)
    return f"Result ({iteration}) from asynchronous API call"

app.layout = html.Div([
    html.H2("Synchronous vs. Asynchronous Functions in Dash"),
    html.Button("Run Sync Tasks", id="sync-btn", style={'margin-right': '10px'}),
    html.Button("Run Async Tasks", id="async-btn"),
    html.Hr(),
    html.Div(id="sync-output", style={'color': 'red', 'font-weight': 'bold'}),
    html.Div(id="async-output", style={'color': 'green', 'font-weight': 'bold'}),
])

@app.callback(
    Output("sync-output", "children"),
    Input("sync-btn", "n_clicks"),
    prevent_initial_call=True,
)
def sync_callback_example(n_clicks):
    if n_clicks:
        results = [get_sync_data(i) for i in range(5)]
        return html.Div([html.Div(result) for result in results])
    return ""

@app.callback(
    Output("async-output", "children"),
    Input("async-btn", "n_clicks"),
    prevent_initial_call=True,
)
async def async_callback_example(n_clicks):
    if n_clicks:
        coros = [get_async_data(i) for i in range(5)]
        results = await asyncio.gather(*coros)
        return html.Div([html.Div(result) for result in results])
    return ""

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

When you install dash[async], Dash installs Flask async’s dependencies.

Note: The gevent worker class with gunicorn is not supported. Using gunicorn with gthread worker class is supported. For example: gunicorn app:app -k gthread --threads=2

:backhand_index_pointing_right: Callback Returned Values Correctly Stored in Persistence

Thank you to @petar-pejovic for the contribution :raising_hands:

This fix ensures that the callback output value is written (instead of being pruned) within the browser’s persistence storage for its dcc components that have persistence=True set. For more details, please see the PR and the issue that Petar’s contribution solves.

Fixed

  • #3341 Fixed query string parsing regression introduced in 2.18.2 where values containing unencoded & characters were being truncated. #3106
  • #3279 Fix an issue where persisted values were incorrectly pruned when updated via callback. Now, callback returned values are correctly stored in the persistence storage. Fix #2678
  • #3298 Fix dev_only resources filtering.
  • #3315 Fix pages module is package check.
  • #3319 Fix issue where ExternalWrapper would remove props from the parent component, now there is a temp that is passed to check if it should be removed on unmount.
  • #3108 Fix layout as list for pages.
  • #1906 Make graph height more responsive.
  • #2927 Fix unexpected behaviour of the cursor in dcc.Input
  • #3344 Fix dcc.Loading target_components with * prop.

Added

  • #3294 Added the ability to pass allow_optional to Input and State to allow callbacks to work even if these components are not in the dash layout.
  • #3077 Add new parameter assets_path_ignore to dash.Dash(). Closes #3076
  • #3202 expose the closeOnSelect option in dropdown component
  • #3089 adding support for async callbacks and page layouts, install with pip install dash[async].

Changed

  • #3303 Improve flatten_grouping performance (callback with dictionary output/input)
  • #3304 Speed up function _operation by 80%
  • #3323 Make Dash instances WSGI compliant, can now call gunicorn on the dash app itself.
10 Likes

Good job, huge changes

1 Like

Optional inputs and state is cool.

Reduces the need to do hacky stuff.

Thanks for making this part of the package!

2 Likes

Amazing! Can’t wait to try this out. I can directly imagine where this can be put to use in my project. :slight_smile:

Kudos to the amazing team and contributors ! :folded_hands:

Love the new updates, especially the optional callback parameter, that will definitely help to have a clean code without too many duplicate callbacks :smiling_face_with_sunglasses:

1 Like

Amazing work, a lot of changes and new features to test!
Congrats ! :heart_eyes:

2 Likes