Unexplained callback behavior

I am trying to understand something in my application callback flow.
I am using pure Dash (2.7.0) and dash-extensions (0.1.8) @Emil

When i have an in Input updating an Output of a different component, my callbacks get triggered everytime i update the children of my parent component. In my example I have a button that adds 2 elements to the UI each time i click it. When I click the new button added, I expect the corresponding Output to update (ONLY when I click it) but it also gets fired everytime I add more buttons

In the screenshot below you can see that I only clicked the add button, but the trigger for each child button gets fired each time. I would only expect the text next to each button to be updated when it is clicked.

Can anyone suggest a workaround/hack or some kind of change I can make to get this to work. The code is written to mock part of my real code so I cant completely rewrite it all

Untitled

image

from dash import html, Output, Input, State, ctx, MATCH, ALL, dcc
from dash_extensions.enrich import DashProxy, OperatorTransform, OperatorOutput, Operator, MultiplexerTransform

app = DashProxy( __name__, transforms=[OperatorTransform(), MultiplexerTransform()], suppress_callback_exceptions=True)

def new_view2(index):
    return html.Div(f"Triggered 0", id={"type": "second-txt", "index": index}, style={"height": "23px"})


def new_view(index):
    return html.Button(f"Button {index}", id={"type": "action-btn", "index": index}, n_clicks=0, style={"height": "23px"})

@app.callback(
    Output({"type": "second-txt", "index": MATCH}, "children"),
    Input({"type": "action-btn", "index": MATCH}, "n_clicks"),
    Input({"type": "second-txt", "index": MATCH}, "children"),
    prevent_initial_call=True
)
def inc_counter_for_view(clicks, triggered):
    return f"Triggered {int(triggered.split()[1]) + 1}"

app.layout = html.Div([
        html.Button("Add View", id="add-view"),
        dcc.Store("items-store", data=[], storage_type="memory"),

        html.Div([
            html.Div([], id="v1", style={"display": "flex", "flex-direction": "column", "gap": "5px"}),
            html.Div([], id="v2", style={"display": "flex", "flex-direction": "column", "gap": "5px"}),
    ],
    style={"display": "flex", "flex-direction": "row", "gap": "5px", "margin-top": "5px"}
        )]
)

app.clientside_callback(
    """
    function(data, v1, v2) {
        v1.push(data.v1)
        v2.push(data.v2)
        return [v1, v2]
    }
    """,
    Output("v1", "children"),
    Output("v2","children"),
    Input("items-store", "data"),
    State("v1","children"),
    State("v2","children"),
    prevent_initial_call=True
)

@app.callback(
    Output("items-store", "data"),
    Input("add-view", "n_clicks"),
    State({"type": "action-card", "index": ALL}, "id"),
    prevent_initial_call=True,
)
def add_view(index, views):
    return {
            "v1": new_view(index),
            "v2": new_view2(index)
        }


app.run_server(debug=False)

Not an expert but I think you would need to use a context and check which item triggered the the actual callback. Based on this you have a logic inside of your callback. refer to advanced callback documentation

section " Determining which Input Has Fired with dash.callback_context

1 Like

I believe callbacks are triggered, when components are added to the layout dynamically. In your clientside callback, you are re-adding all the buttons to the layout, thus triggering a callback for each of them.

I dont think this is entirely true…If i change the callback to be like below, then it does not trigger every time a new button is added to layout.

@app.callback(
    Output({"type": "action-btn", "index": MATCH}, "children"),
    Input({"type": "action-btn", "index": MATCH}, "n_clicks"),
    Input({"type": "action-btn", "index": MATCH}, "children"),
    prevent_initial_call=True
)

instead of

@app.callback(
    Output({"type": "second-txt", "index": MATCH}, "children"),
    Input({"type": "action-btn", "index": MATCH}, "n_clicks"),
    Input({"type": "second-txt", "index": MATCH}, "children"),
    prevent_initial_call=True
)

Unfortunatelly the callback context indicates it was n_clicks that triggered the callback…when it wasn’t. The value of the triggered prop remains 0 but the callback is triggered

Hey @dales,

Same issue different day. Haha.

The only way I can think of is to store the total of n_clicks and compare against it each time. This should make sure that you don’t update unless there is an actual increase in the value triggers.

You could probably just use a memory store to accomplish this.

It is a different part of my app…similar behaviour to previous issue I have raised but in a different part with a different reason for triggering the callbacks…and I was hoping that this example would be somehow different. My biggest problem with some kind of timestamp hack is mainly due to the nature of my application. Several other teams will be writing plugins to the system for various visualisations. So I was looking for a solution that would not need them to implement something like this for each visualization.

I wonder why there can’t be some indication in the ctx that that was not a direct callback trigger rather from layout update. Like ctx.triggered_initiator or something

1 Like

@dales,

I had another thought, what if, whenever you went to add another element, you reset all the n_click values to 0 of all the children.

Then, in your callback, you check to make sure n > 0.

This would mean that whoever writes a plugin needs to worry about these kind of behaviors which I am trying to avoid

What I dont understand is why this does not reproduce if move the button to have a shared parent DIV vs having it placed somewhere else. The callbacks do not change, the only thing that changes is there the output-txt DIV is moved

@AnnMarieW, @adamschroeder do you have any explanation as to why this occurs in this case?

THIS DOES NOT CAUSE CALLBACKS TO FIRE WHEN NEW ELEMENTS ADDED TO LAYOUT
gif2

def right_view(index):
    return html.Div([
    ])


def left_view(index):
    return html.Div([
        html.Button(f"Button {index}", id={"type": "action-btn", "index": index}, n_clicks=0, style={"height": "23px"}),
        html.Div(f"Triggered 0", id={"type": "output-txt", "index": index}, style={"height": "23px"})
    ])

THIS DOES CAUSE CALLBACKS TO FIRE WHEN NEW ELEMENTS ADDED TO LAYOUT
gif1

def right_view(index):
    return html.Div([
        html.Div(f"Triggered 0", id={"type": "output-txt", "index": index}, style={"height": "23px"})
    ])


def left_view(index):
    return html.Div([
        html.Button(f"Button {index}", id={"type": "action-btn", "index": index}, n_clicks=0, style={"height": "23px"}),
    ])

FULL CODE

from dash import html, Output, Input, State, ctx, MATCH, ALL, dcc, Dash

app = Dash( __name__, suppress_callback_exceptions=True)

def right_view(index):
    return html.Div([
        # This causes callbacks to fire whenever added to the layout
        html.Div(f"Triggered 0", id={"type": "output-txt", "index": index}, style={"height": "23px"})
    ])


def left_view(index):
    return html.Div([
        html.Button(f"Button {index}", id={"type": "action-btn", "index": index}, n_clicks=0, style={"height": "23px"}),
        # Moving the div here does not cause the callbacks to fire when added to the layout
        # html.Div(f"Triggered 0", id={"type": "output-txt", "index": index}, style={"height": "23px"})
    ])

@app.callback(
    Output({"type": "output-txt", "index": MATCH}, "children"),
    Input({"type": "action-btn", "index": MATCH}, "n_clicks"),
    Input({"type": "output-txt", "index": MATCH}, "children"),
    prevent_initial_call=True
)
def inc_counter_for_view(clicks, triggered):
    return f"Triggered {int(triggered.split()[1]) + 1}"

app.layout = html.Div([
        html.Button("Add View", id="add-view"),
        dcc.Store("items-store", data=[], storage_type="memory"),

        html.Div([
            html.Div([], id="left", style={"display": "flex", "flex-direction": "column", "gap": "5px"}),
            html.Div([], id="right", style={"display": "flex", "flex-direction": "column", "gap": "5px"}),
    ],
    style={"display": "flex", "flex-direction": "row", "gap": "5px", "margin-top": "5px"}
        )]
)

app.clientside_callback(
    """
    function(data, left, right) {
        left.push(data.left)
        right.push(data.right)
        return [left, right]
    }
    """,
    Output("left", "children"),
    Output("right","children"),
    Input("items-store", "data"),
    State("left","children"),
    State("right","children"),
    prevent_initial_call=True
)

@app.callback(
    Output("items-store", "data"),
    Input("add-view", "n_clicks"),
    prevent_initial_call=True,
)
def add_view(index):
    return {
            "left": left_view(index),
            "right": right_view(index)
        }

app.run_server(debug=True)

Hi @dales

I ran your code and could replicate the issue. I’m not sure why it behaves differently depending on the which div the output-txt in, but maybe it’s the timing of when it’s added? (Just guessing)

You could check for n_clicks > 0 . I see your Triggered #message counter is updated manually in your example but the n_clicks remains at 0

Ooo, this is interesting.

As a rule of thumb, I always check to see if what I am using to trigger info actually exists.

However, checking to see if the value is above 0 or there wont work once you click a button once and then add it back. The n_clicks does not increase though.

So, if I do this, the value doesnt increase, but the callback is still triggered, I’m assuming due to the nature and reason why prevent_initial_call is necessary in most cases:

@app.callback(
    Output({"type": "output-txt", "index": MATCH}, "children"),
    Input({"type": "action-btn", "index": MATCH}, "n_clicks"),
    Input({"type": "output-txt", "index": MATCH}, "children"),
    prevent_initial_call=True
)
def inc_counter_for_view(clicks, triggered):
    if clicks:
        return f"Triggered {clicks}"
    return dash.no_update

prevent_initial_call will not work in this case because the layout has already loaded as we are adding to it.

This seems like an issue with outputs, and how they are parsed, but not even sure how you would go about tackling this…

Glad everyone sees the problem. And I can’t check the callback context because I don’t know how to differentiate between active and passive triggering

As @jinnyzor mentioned. Once the user interacts with the button then n_clicks will be > 0 so that check won’t work in this case.

2 Likes

A crazy idea… not sure if it has any value though. What happens to disabled buttons? will they trigger a click/callback event when added? in theory it shouldn’t be possible. so imagine you add them disabled and then enable them through a client callback with a timer of 250 ms or something? Might still generate the callback then… but it might be worth a try

Nice thought, but this does not work.

Just as functions can be called on webpages even if the button is disabled, this still triggers a callback with the buttons disabled.

(disabling a button only works with people who play nice) :rofl:

Well, true the function is called but now you can pass in disabled as a state and block the update :slight_smile: suboptimal but nice people can get mean too :slight_smile:

@jcuypers,

I tried it, it still triggers the callback. Even with a disabled prop. :wink:

i know, but this is where the disabled state comes in for the matching of the action buttons. if disabled dont fire

@app.callback(
    Output({"type": "output-txt", "index": MATCH}, "children"),
    Input({"type": "action-btn", "index": MATCH}, "n_clicks"),
    Input({"type": "output-txt", "index": MATCH}, "children"),
    State({"type": "action-btn", "index": MATCH}, "disabled"),
    prevent_initial_call=True
)
def inc_counter_for_view(clicks, triggered, disabled):

    if not disabled:
        return f"Triggered {int(triggered.split()[1]) + 1}"
    else: 
        return f"Triggered {int(triggered.split()[1])}"

Unfortunately, unless you were to have a button that toggles it independently, you end up with a circular callback with this. If you are really interested in a workaround:

from dash import html, Output, Input, State, ctx, MATCH, ALL, dcc, Dash
import dash

app = Dash( __name__, suppress_callback_exceptions=True)

def right_view(index):
    return html.Div([
        #html.Button(f"Button {index}", id={"type": "action-btn", "index": index}, n_clicks=0, style={"height": "23px"})
        # This causes callbacks to fire whenever added to the layout
        html.Div(f"Triggered 0", id={"type": "output-txt", "index": index}, style={"height": "23px"})
    ])


def left_view(index):
    return html.Div([
        html.Div([html.Button(f"Button {index}", id={"type": "action-btn", "index": index},
                              n_clicks=0, style={"height": "23px"})]),
        # Moving the div here does not cause the callbacks to fire when added to the layout
        #html.Div([html.Div(f"Triggered 0", id={"type": "output-txt", "index": index}, style={"height": "23px"})])

    ])

@app.callback(
    Output({"type": "output-txt", "index": MATCH}, "children"),
    Input({"type": "action-btn", "index": MATCH}, "n_clicks"),
    Input({"type": "output-txt", "index": MATCH}, "children"),
    State('added-store', 'data'),
    State({"type": "action-btn", "index": ALL}, "n_clicks"),
    prevent_initial_call=True
)
def inc_counter_for_view(clicks, triggered, s, _):
    if s != sum(_):
        return f"Triggered {int(triggered.split()[1]) + 1}"
    return dash.no_update

@app.callback(
    Output('added-store', 'data'),
    Input({"type": "action-btn", "index": ALL}, "n_clicks"),
)
def updateLast(n):
    return sum(n)

app.layout = html.Div([
        html.Button("Add View", id="add-view"),
        dcc.Store("items-store", data=[], storage_type="memory"),
        dcc.Store('added-store', data=[], storage_type='memory'),

        html.Div([
            html.Div([], id="left", style={"display": "flex", "flex-direction": "column", "gap": "5px"}),
            html.Div([], id="right", style={"display": "flex", "flex-direction": "column", "gap": "5px"}),
    ],
    style={"display": "flex", "flex-direction": "row", "gap": "5px", "margin-top": "5px"}
        )]
)

app.clientside_callback(
    """
    function(data, left, right) {
        left.push(data.left)
        right.push(data.right)
        return [left, right]
    }
    """,
    Output("left", "children"),
    Output("right","children"),
    Input("items-store", "data"),
    State("left","children"),
    State("right","children"),
    prevent_initial_call=True
)

@app.callback(
    Output("items-store", "data"),
    Input("add-view", "n_clicks"),
    prevent_initial_call=True,
)
def add_view(index):
    return {
            "left": left_view(index),
            "right": right_view(index)
        }

app.run_server(debug=True, port=12345)

This uses a dcc.Store to store the values of the n_clicks, if the value doesnt increase, then there is no update.

@dales,

I think you should make an issue on github. It seems to be associated with parsing info with different outputs in the same callback.

1 Like