Using current content of `Output` within callback to prevent unnecessary updates

I have a multi-page dash app with a variaty of inputs for some charts and have been racking my brain for a way to prevent callbacks from being unnecessarily executed multiple times in a row in various scenarios.

None of the solutions I have come up with (hidden Divs, pickles, invisible buttons etc.) have been satisfying, since all of them were either unable to cover all scenarios or broke the app in some other way.

IMO, the simplest way to achieve this would be to check the current content of the targeted Output element and just raise PreventUpdate if the content would be the same before and after the callback. However, Dash doesn’t currently allow this, as it raises an error message if I use an element as both Output and Input for a callback.

Is there a way to circumvent this error message? Or some other solution that I haven’t thought of?

The issue here is more that you cannot use the same element as an Output more than once.
So if there already is content in that div you cannot output to that again.
Yes, I find this extremely annoying myself and hope we will be able to do that at some point in the future…

I don’t know what exactly you want to do, but maybe you can check what state, or input is changed/triggered and do something with that?
I’ll leave these two links here, maybe it helps:

FAQs | Dash for Python Documentation | Plotly (scroll down to " Q: How do I determine which Input has changed?")

Thanks for the links! I’m using the callback context already, but some callbacks are still fired multiple times because they are chained and all get executed in succession if the topmost callback in the chain is fired.

Actually this can be somewhat decently solved with n_clicks, preventing all callbacks until something has been clicked on the site, but I need to use dcc.Dropdown, not dcc.Button, and dcc.Dropdown don’t support n_clicks (maybe a feature suggestion for the devs).

Can you elaborate on this? I’m not sure what you mean.

E.g., callbacks can overwrite the content of an existing div, as long as there is only ever one callback with that div as Output. As far as I can see, that’s an independent issue from being able to use a div as both Input and Output, since this would all be done within a single callback.

You are right, I think I misunderstood your application.
But that would mean that you are innately looping your callback infinitely, because it will always be triggered when it is giving an output.
Why do you want to do that, instead of providing the same object as Output and State instead?
Maybe you can explain this a bit more, it is hard to think about a solution like this.

As i understand your question, you simply would like to stop the update, if the content of the object has not changed? If this is the case, you can simply include the object as state (and output), do the appropriate checks, and raise the update error (or return the previous state) if no update is needed.

See this is what happens if you dig into a problem until you can’t see the simple mistakes anymore ;).

Anyways, I just tried to work with this, but it seems like plotly automatically creates a random uid for Graph objects, which is never the same for two Graphs - so comparing State and the intended output doesn’t work without some complicated and expensive regex trickery.

Edit: Actually this doesn’t work, even with regex, since Graph objects contain dictionaries that aren’t always in the same order when converting to a string. I’ve also tried to set a custom uid, but that gets ignored by plotly. Back to square 1.

Not sure I entirely follow your use case, but just to your last point: you can avoid the Graph uid issue either by constructing the figure as a plain dict ({'data': [...], 'layout': {...}} instead of a go.Figure) or, I believe, by updating to plotly.py v4 https://github.com/plotly/plotly.py/issues/1512

I use a submit button to avoid sending multiple callbacks into the queue. If Submit isn’t clicked then nothing happens. Once Submit is clicked on then it does something and immediately resets the n_clicks value back to None (or zero probably works too). Afterwards, nothing will happen unless Submit is clicked for that particular callback. If you DON’T reset the value, you’ll find that you can only click the Submit button once and have it work right. And, without resetting the n_clicks value, any change that would normally trigger the callback will not wait for the Submit button (obviously not what you want). Hope that makes sense.

@app.callback(
    [Output('graph-id', 'figure'), Output('submit-button','n_clicks')],
    [Input('slider-widget', 'value'),
     Input('toggle-button', 'value'),
     Input('submit-button','n_clicks')])
def function(slider_value, toggle_value , n_clicks):
    if n_clicks is None:
            return dash.no_update
    else:
        print('n_clicks', n_clicks)
        <do some other stuff>
        return fig, None

Nice, making use of the brand new (Dash 1.19) circular callback support - ie the same prop submit-button.n_clicks is both an input and an output :slight_smile:

But normally the pattern we use for this kind of situation is to call the submit button an Input and the other parameters State - then this is a lot simpler and doesn’t require resetting n_clicks. You do need prevent_initial_call though in order to avoid the if n_clicks is None:

@app.callback(
    Output('graph-id', 'figure'),
    Input('submit-button','n_clicks'),
    State('slider-widget', 'value'),
    State('toggle-button', 'value'),
    prevent_initial_call=True
)
def function(n_clicks, slider_value, toggle_value):
    <do some other stuff>
    return fig

Does that behave the way you want?

1 Like

Hi Alex, I tried your suggestion in place of mine but “prevent_initial_call” was not preventing the initial call. In my scenario (perhaps in contrast to the original poster’s) I don’t want the graph to update at all until the Submit button is pressed. But it does update the very first time. Thereafter it works as it’s supposed to, only triggering when Submit is pressed. Any ideas? Thanks!

“prevent_initial_call” was not preventing the initial call.

Can you post a simplified complete app showing this behavior? This can happen if for example some other inputs to this callback are themselves outputs of other callbacks that are NOT prevented, but I don’t think that can be the case here as you only have one Input. I think you may also be able to generate this situation if submit-button is in a part of the layout that’s created after graph-id is already on the page, any chance that applies here? Regardless of how it happened, you should be able to solve it by adding back in the if n_clicks is None: return dash.no_update but still without including the n_clicks reset.

Yeah, you’re right. I could remove the n_clicks reset and it was still ok. Thanks!

@app.callback(
    Output('simulation', 'figure'),
    Input('t-file', 'value'), Input('submit-button','n_clicks'),
    State('h-slider', 'value'),State('t-slider', 'value'),State('h-toggle', 'on'),
    prevent_initial_call=True)
def simulation_graph(filename, n_clicks, h, g_time, cv_h):
    if n_clicks is None:
        return dash.no_update
    else:
        start = g_time
        stop = g_time + 5
        fig = simulator(filename, h, start, stop, cv_h)
        fig.layout.updatemenus[0].buttons[0].args[1]["frame"]["redraw"] = True
        fig.layout.updatemenus[0].buttons[1].args[1]["frame"]["redraw"] = True
        return fig

Ah, but you have another input Input('t-file', 'value') - try changing that one to State as well so the button is the only Input and everything else is State.

I tried the following but it still triggers an initial graph generation upon loading, which is what we’re all trying to avoid (or at least I am)

@app.callback(
    Output('simulation', 'figure'),
    Input('submit-button','n_clicks'), State('t-file', 'value'),
    State('h-slider', 'value'), State('t-slider', 'value'), State('h-toggle', 'on'),
    prevent_initial_call=True)
def simulation_graph(n_clicks, filename, h, g_time, cv_h):
    #if n_clicks is None:
    #    return dash.no_update
    #else:
    start = gametime
    stop = gametime + 5
    fig = simulator(filename, h, start, stop, cv_h)
    fig.layout.updatemenus[0].buttons[0].args[1]["frame"]["redraw"] = True
    fig.layout.updatemenus[0].buttons[1].args[1]["frame"]["redraw"] = True
    return fig

Uncommenting the ‘if n_clicks’ bit causes it to work properly (no initial trigger, only when I click Submit does it graph). Not sure why it’s behaving this way. But it’s working for me like this, so I’m not too concerned on my side.