Callbacks working individually but not together

I have the following 2 functions:

@app.callback(
    [Output('memory', 'data'), Output('linesDiv', 'children')],
    Input('lineButton', 'n_clicks'),
    [State('memory', 'data'), State('linesDiv', 'children')],
    prevent_initial_call=True)
def addLine(n, mem, div):
    if n is None: raise PreventUpdate
    print(2)
    mem['n'] += 1
    mem['lines'].append(go.Scatter(x=[0, 1, 2, 3, 4], y=[0, 1, 2, 3, 4], mode='lines', name=f'Line {mem["n"]}'))
    div += [html.Div([
        html.Label(f"Line {mem['n']}"),
        dbc.Button('Delete', 
                   id={'type': 'deleteLineButton', 'index': mem["n"]}, 
                   className='deleteLineButton'),
        dcc.Dropdown(id={'type': 'lineDropdown', 'index': mem["n"]}, 
                     options=list(SLIDERS.keys())),
    ], className='lineWidgetDiv', id={'type':'lineWidgetDiv', 'index':mem['n']})]
    
    return mem, div

@app.callback(
    [Output('memory', 'data'), Output('linesDiv', 'children')], 
    [Input({'type': 'deleteLineButton', 'index': ALL}, 'n_clicks')],
    [State('memory', 'data'), State('linesDiv', 'children')],
    prevent_initial_call=True)
def deleteLine(n, mem, div):
    if n is None: raise PreventUpdate
    print(23)
    ident = eval(dash.callback_context.triggered[0]['prop_id'].split('.')[0])['index']
    for i in mem['lines']:
        if i['name'] == f"Line {ident}":
            mem['lines'].remove(i)
            break
    else: raise ValueError('Line not found')
    
    for i in div:
        if i['props']['children'][1]['props']['id']['index'] == ident:
            div.remove(i)
            break
    else: raise ValueError("Not found")
    
    return mem, div

The first function creates a new “widget” which contains a delete line button. The second function should be triggered when the delete line button is triggered and should delete the widget. When one function is commented, the other works fine. However, when both are uncommented, neither function works (no values are printed, and there’s no error).

If more than one callback outputs to the same property of the same element, you have to add allow_duplicate=True (and prevent_initial_call=True, but you already have that). So you need:

@app.callback(
    [Output('memory', 'data', allow_duplicate=True), 
  Output('linesDiv', 'children', allow_duplicate=True)],
  ...

Hi, thanks for your reply. The code you’ve suggested now causes one of the functions to work (the one without the allow_duplicate=True, but the other button still doesn’t work.
Adding allow_duplicate to both functions means neither works

This might cause trouble. Since you are using ALL, n is actually a list. So you have to check with any() or all() depending on your condition.

Thanks, but I’ve already changed this, but it still produces the same problem: the deleteLine function works fine, but the addLine doesn’t work at all. My code is currently as follows:

@app.callback(
    [Output('memory', 'data', allow_duplicate=True), Output('linesDiv', 'children', allow_duplicate=True)],
    Input('lineButton', 'n_clicks'),
    [State('memory', 'data'), State('linesDiv', 'children')], 
    prevent_initial_call=True)
def addLine(n, mem, div):
    if n is None: raise PreventUpdate
    print(2)
    mem['n'] += 1
    mem['lines'].append(go.Scatter(x=[0, 1, 2, 3, 4], y=[0, 1, 2, 3, 4], mode='lines', name=f'Line {mem["n"]}'))
    div += [html.Div([
        html.Label(f"Line {mem['n']}"),
        dbc.Button('Delete', 
                   id={'type': 'deleteLineButton', 'index': mem["n"]}, 
                   className='deleteLineButton'),
        dcc.Dropdown(id={'type': 'lineDropdown', 'index': mem["n"]}, 
                     options=list(SLIDERS.keys())),
    ], className='lineWidgetDiv', id={'type':'lineWidgetDiv', 'index':mem['n']})]
    
    return mem, div

@app.callback(
    [Output('memory', 'data'), Output('linesDiv', 'children')], 
    [Input({'type': 'deleteLineButton', 'index': ALL}, 'n_clicks')],
    [State('memory', 'data'), State('linesDiv', 'children')],
    prevent_initial_call=True)
def deleteLine(n, mem, div):
    if not any(n): raise PreventUpdate
    print(23)
    ident = eval(dash.callback_context.triggered[0]['prop_id'].split('.')[0])['index']
    for i in mem['lines']:
        if i['name'] == f"Line {ident}":
            mem['lines'].remove(i)
            break
    else: raise ValueError('Line not found')
    
    for i in div:
        if i['props']['children'][1]['props']['id']['index'] == ident:
            div.remove(i)
            break
    else: raise ValueError("Not found")
    
    return mem, div

Hello @AG-88301,

I bet that this is working, but you have inadvertently created a chained callback.

When using an ALL, anytime a component is added to a div, it will trigger the callbacks of each component in the div, regardless of the prevent initial call. To see this illustrated, work in dev mode and look at the callback graph to see that the delete line gets called immediately after being added.

To prevent this, you need to loop through the values of the ALL list and check for the number 1. Use that to determine whether you should remove the line.

As an aside, have you considered using Patch instead of storing info in the store?

Hi, if I understand you correctly, you mean I should try doing something like this in the deleteLine function…?:

ident = eval(dash.callback_context.triggered[0]['prop_id'].split('.')[0])['index']
if ident not in n: raise PreventUpdate

I’ve just tried this, but unfortunately nothing’s changed, the addLine button still isn’t working.
I think the problem’s with the output memory/div not updating as specified in the callback, but I’m not too sure why.
As for using a Patch, I’ll have a look into it.
Thank you for your help so far…

[full code repeated below]

# function not working
@app.callback(
    [Output('memory', 'data', allow_duplicate=True), Output('linesDiv', 'children', allow_duplicate=True)],
    Input('lineButton', 'n_clicks'),
    [State('memory', 'data'), State('linesDiv', 'children')], 
    prevent_initial_call=True)
def addLine(n, mem, div):
    if n is None: raise PreventUpdate
    print(2)
    mem['n'] += 1
    mem['lines'].append(go.Scatter(x=[0, 1, 2, 3, 4], y=[0, 1, 2, 3, 4], mode='lines', name=f'Line {mem["n"]}'))
    div += [html.Div([
        html.Label(f"Line {mem['n']}"),
        dbc.Button('Delete', 
                   id={'type': 'deleteLineButton', 'index': mem["n"]}, 
                   className='deleteLineButton'),
        dcc.Dropdown(id={'type': 'lineDropdown', 'index': mem["n"]}, 
                     options=list(SLIDERS.keys())),
    ], className='lineWidgetDiv', id={'type':'lineWidgetDiv', 'index':mem['n']})]
    
    return mem, div


# function working
@app.callback(
    [Output('memory', 'data'), Output('linesDiv', 'children')], 
    [Input({'type': 'deleteLineButton', 'index': ALL}, 'n_clicks')],
    [State('memory', 'data'), State('linesDiv', 'children')],
    prevent_initial_call=True)
def deleteLine(n, mem, div):
    print(n)
    ident = eval(dash.callback_context.triggered[0]['prop_id'].split('.')[0])['index']
    if 1 not in n: raise PreventUpdate
    print(23)
    for i in mem['lines']:
        if i['name'] == f"Line {ident}":
            mem['lines'].remove(i)
            break
    else: raise ValueError('Line not found')
    
    for i in div:
        if i['props']['children'][1]['props']['id']['index'] == ident:
            div.remove(i)
            break
    else: raise ValueError("Not found")
    
    return mem, div

To clarify, this is not a full working example that anyone can run.

To check to see what was actually clicked, you need to look at the actual values. Not use the ctx triggered id. Which there is an easier way to get to that too.

ctx.triggered_id.index

Or something similar.

Hi, sorry I forgot to mention a couple details / didn’t mention them clearly enough:

  1. I’m unable to access the dev tools, since I’m using django-plotly-dash, which as far as I know doesn’t have access to them (please correct me if I’m wrong)
  2. The functions are being called fine (the prints are working), the problem is with the Outputs which are not showing. I know the Input and State callbacks work fine, as prior to opening this question, I’ve tested both functions individually, with the other commented out. The problem is whichever function now has the allow_duplicate=True doesn’t Output anything, it just executes, but the Output isn’t shown.

If you believe the full example would be helpful, I’m happy to provide a GitHub link to the full code.
Thanks for your help.

Does the delete one trigger immediately after adding a new line in python console, check via the print statement.

Also, Django dash can lag behind the main dash at times. So be wary of that as well.

No, pressing one button triggers only one print statement

It’s not hitting the value raise error?

Have you tried wrapping the outputs as the lists that they are?

Normally, it can be a little finicky about the outputs needing to match the definition that you provided.

But the function works fine when the other one is commented (and if allowDuplicate is removed), so I’m assuming the definitions are correct…?

If you said that the button pushes are triggering the callback successfully, via the print statements, then the issue is with the callback response.

It is also possible that Django dash is not updated to use allow_duplicate…