Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
marchevska opened this issue Aug 23, 2019 · 6 comments
Closed

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

marchevska opened this issue Aug 23, 2019 · 6 comments

Comments

@marchevska
Copy link

marchevska commented Aug 23, 2019

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

@alexcjohnson
Copy link
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?

@adrianoesch
Copy link
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

@adrianoesch
Copy link
Contributor

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

@adrianoesch
Copy link
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)

@ned2
Copy link
Contributor

ned2 commented Nov 5, 2022

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.

@alexcjohnson
Copy link
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
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants