Skip to content

[BUG] Callback with dcc.Location 'search' input is fired 2 times  #883

Closed

Description

@marchevska

Hi, I noticed a small issue with dcc.Location. Callbacks having Input('url', 'search') are fired twice with every change of the parameter. Below is a small demo

import dash
import dash_html_components as html
import dash_core_components as dcc
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.layout = html.Div([
    dcc.Location(id='url', refresh=False),
    html.Div(dcc.Input(id='input-box', type='text')),
    html.Div(html.Button('Submit', id='button')),
    html.Div('', id='div1'),
    html.Div('', id='div2'),
])


@app.callback(
    Output('url', 'search'),
    [Input("button", "n_clicks")],
    [State("input-box", "value")]
)
def update_url(n, value):
    if not n:
        raise dash.exceptions.PreventUpdate

    print(f'update url, n_clicks={n} value={value}')
    return f'?search={value}'


# This callback is called twice
@app.callback(
    Output('div1', 'children'),
    [Input('url', 'search')]
)
def update_div1(s):
    print(f'update div1 {s}')
    return s


@app.callback(
    Output('div2', 'children'),
    [Input('div1', 'children')]
)
def update_div2(s):
    print(f'update div2 {s}')
    return s


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

Each time the button is submitted, the output is like this, which shows that update_div1 function runs twice

update url, n_clicks=1 value=123
update div1 ?search=123
update div1 ?search=123
update div2 ?search=123

Environment

python 3.6
dash==1.1.1
dash-core-components==1.1.1
dash-daq==0.1.0
dash-html-components==1.0.0
dash-renderer==1.0.0
dash-table==4.1.0

Activity

  1. alexcjohnson commented on Aug 23, 2019

    @alexcjohnson
    Collaborator

    Good catch - and thanks for the nice simplified demo @marchevska 🏆

    Looks to me as though there are 2 things going on here, either of which would fix this. We might want to do both, but we need to investigate potential side-effects:

    • dcc.Location is calling setProps in response to componentWillReceiveProps - that seems weird, if we're in componentWillReceiveProps these changes are already reflected in the layout prop tree, right?
    • In the renderer we're updating props and kicking off callbacks even if the props in question didn't actually change. Mostly components manage this themselves, avoiding a setProps call if nothing changed; but not always (for example relayoutData @byronz ) - is there any reason we couldn't short-circuit the update if nothing actually changed?
  2. adrianoesch commented on Sep 9, 2019

    @adrianoesch
    Contributor

    I have observed similar behaviour when updating inputs with the location href data.

    Example:

    from dash import Dash
    import dash_core_components as dcc
    import dash_html_components as html
    from dash.dependencies import Input, Output
    from dash.exceptions import PreventUpdate
    
    dash = Dash(__name__)
    dash.layout=html.Div([
        dcc.Input(id='var1'),
        dcc.Input(id='var2'),
        dcc.Input(id='var3'),
        dcc.Input(id='var4'),
        html.Div(id='result'),
        dcc.Location(id='url',refresh=False)
    ])
    
    @dash.callback([
              Output('var1', 'value'),
              Output('var2', 'value'),
              Output('var3', 'value'),
              Output('var4', 'value')
          ],[
              Input('url','href')
           ])
    def update_inputs(href):
        if href is None or not '?vars=' in href:
            raise PreventUpdate
        vars = href.split('?vars=')[-1].split(',')
        print('update_inputs')
        return vars
    
    @dash.callback([
          Output('result', 'children'),
          Output('url', 'search')
      ],[
          Input('var1', 'value'),
          Input('var2', 'value'),
          Input('var3', 'value'),
          Input('var4', 'value'),
       ])
    def update_result(var1,var2,var3,var4):
        vars = [var1,var2,var3,var4]
        vars = ['' if i is None else i for i in vars]
        result = ','.join(vars)
        search = '?vars='+result
        print(vars)
        print('update_result')
        return result,search
    
    if __name__ == '__main__':
        dash.run_server(debug=True)
    

    When visiting http://localhost:8050/?vars=a,b,c,d the inputs are being updated once, but the result is updated sequentially four times, instead of all at once.

    Output:

    Running on http://127.0.0.1:8050/
    Debugger PIN: 227-990-801
    update_inputs
    ['a', '', '', '']
    update_result
    ['a', 'b', '', '']
    update_result
    ['a', 'b', 'c', '']
    update_result
    ['a', 'b', 'c', 'd']
    update_result
    

    Env:

    python 3.7
    dash==1.2.0
    dash-core-components==1.1.2
    
  3. adrianoesch commented on Sep 9, 2019

    @adrianoesch
    Contributor

    Is there a way of aborting sequential updates and only letting the last one complete as a quick fix?

  4. adrianoesch commented on Sep 9, 2019

    @adrianoesch
    Contributor

    A possible quick fix could be using the flask session to flag the location as a trigger source and dash_context.trigger to get the most recent prop that is updated and then only allow the last update to complete.

    from flask import Flask, session
    from flask import Session
    from dash import Dash
    import dash_core_components as dcc
    import dash_html_components as html
    from dash.dependencies import Input, Output
    from dash.exceptions import PreventUpdate
    from dash import callback_context
    
    flask = Flask(__name__)
    flask.secret_key = 'test'
    dash = Dash(__name__,flask)
    dash.layout=html.Div([
        dcc.Input(id='var1'),
        dcc.Input(id='var2'),
        dcc.Input(id='var3'),
        dcc.Input(id='var4'),
        html.Div(id='result'),
        dcc.Location(id='url',refresh=False)
    ])
    
    @dash.callback([
              Output('var1', 'value'),
              Output('var2', 'value'),
              Output('var3', 'value'),
              Output('var4', 'value')
          ],[
              Input('url','href')
           ])
    def update_inputs(href):
        if href is None or not '?vars=' in href:
            raise PreventUpdate
        vars = href.split('?vars=')[-1].split(',')
        session['is_url_trigger'] = True
        print('update_inputs')
        return vars
    
    @dash.callback([
          Output('result', 'children'),
          Output('url', 'search')
      ],[
          Input('var1', 'value'),
          Input('var2', 'value'),
          Input('var3', 'value'),
          Input('var4', 'value'),
       ])
    def update_result(var1,var2,var3,var4):
        is_url_trigger = session.get('is_url_trigger')
        trigger_prop = callback_context.triggered[0]['prop_id'].split('.')[0]
        if is_url_trigger and not trigger_prop == 'var4':
            raise PreventUpdate
        if is_url_trigger and trigger_prop == 'var4':
            session['is_url_trigger']=False
        vars = [var1,var2,var3,var4]
        vars = ['' if i is None else i for i in vars]
        result = ','.join(vars)
        search = '?vars='+result
        print(vars)
        print('update_result')
        return result,search
    
    if __name__ == '__main__':
        dash.run_server(debug=True)
    
  5. ned2 commented on Nov 5, 2022

    @ned2
    Contributor

    I've taken the original example for a spin, and it's no longer a problem in Dash (2.7.0). Bisecting reveals that the problem was resolved in Dash 1.11.0, which the change-log indicates was when Pattern Matching Callbacks was added, which apparently fixed a bunch of callback issues, including callbacks being triggered multiple times.

    #1103 Fixed multiple bugs with chained callbacks either not triggering, inconsistently triggering, or triggering multiple times. This includes: #635, #832, #1053, #1071, and #1084. Also fixed #1105: async components that aren't rendered by the page (for example in a background Tab) would block the app from executing callbacks.

    @alexcjohnson I have a hunch, that like #1049, this bug may have now been resolved.

  6. alexcjohnson commented on Nov 8, 2022

    @alexcjohnson
    Collaborator

    Thanks @ned2! I agree, this one can be closed 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions