So, are circular references really "bad"? I would rather say that forbidding them is a consequence of the Dash principal to have each callback make a change in the resulting web page. But in fact, Dash is not consequent on this principal already:
- Dash defines a PreventUpdate exception that one can raise when the application's state does not require an update of the web page. This would be the pythonic way to handle intended circular references and is suggested in the previously linked Dash issue.
- From the Dash gotchas: "If you have disabled callback validation in order to support dynamic layouts, then you won't be automatically alerted to the situation where a component within a callback is not found within a layout. In this situation, where a component registered with a callback is missing from the layout, the callback will fail to fire. For example, if you define a callback with only a subset of the specified
Inputs
present in the current page layout, the callback will simply not fire at all."
Exploitation of the Dash gotcha works by including a Dash component in the loop, in the code example below the html.Div with id='loop_breaker'. This html.Div is dynamically generated inside the static html.Div with id='loop_breaker_container'. The 'loop_breaker' component is only generated when:
- the user has deselected all options while the "all" checkbox is still checked
- the user has selected all options while the "all" checkbox is still unchecked
# -*- coding: utf-8 -*- import dash import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output, State external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) app.config['suppress_callback_exceptions'] = True app.layout = html.Div(children=[ html.H4(children='Excel-like checkboxes'), dcc.Checklist( id='all', options=[{'label': 'all', 'value': 'all'}], values=[] ), dcc.Checklist( id='cities', options=[ {'label': 'New York City', 'value': 'NYC'}, {'label': 'Montréal', 'value': 'MTL'}, {'label': 'San Francisco', 'value': 'SF'} ], values=['MTL', 'SF'] ), html.Div(id='loop_breaker_container', children=[]) ]) @app.callback(Output('cities', 'values'), [Input('all', 'values')]) def update_cities(inputs): if len(inputs) == 0: return [] else: return ['NYC', 'MTL', 'SF'] @app.callback(Output('loop_breaker_container', 'children'), [Input('cities', 'values')], [State('all', 'values')]) def update_all(inputs, _): states = dash.callback_context.states if len(inputs) == 3 and states['all.values'] == []: return [html.Div(id='loop_breaker', children=True)] elif len(inputs) == 0 and states['all.values'] == ['all']: return [html.Div(id='loop_breaker', children=False)] else: return [] @app.callback(Output('all', 'values'), [Input('loop_breaker', 'children')]) def update_loop(all_true): if all_true: return ['all'] else: return [] if __name__ == '__main__': app.run_server(debug=True)