Skip to content

callback triggered while dash.callback_contract.triggered is [{'prop_id': '.', 'value': None}] #1523

Open
@sdementen

Description

@sdementen

Describe your context

I am trying to use a Store that could be written by multiple callbacks (to circumvent the 1 output can only be updated by one callback). The logic (hack?) looks sound and works except that I get a problem with a callback that is triggered with
dash.callback_contract.triggered == [{'prop_id': '.', 'value': None}] each time an unrelated Div is updated (ie not only on the first firing at the beginning of the app) and is moreover triggered twice.

Here is the sample app with the get_multi_store function generating a Div with multiple stores (one core and one per writer).

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State

app = dash.Dash()


def get_multi_store(id_store, id_writers, mode="replace"):
    """Generate a store that can be written by multiple writers.

    For input, use Input(id_store, "data").
    For output by writer 'x', use Output(id_store+"-x", "data").

    If mode=="replace", the data of the store is replaced when written to it.
    If mode=="append", the data of the store is appended with the new data.

    """
    assert mode in {"replace", "append"}
    N = len(id_writers)

    id_container = f"{id_store}-container"
    id_store_root = id_store
    id_store_writers = [f"{id_store}-{id_writer}" for id_writer in id_writers]


    def get_layout(data):
        """Generate the layout with the root read-only store and the writable stores"""
        return [dcc.Store(id=id_store_root, data=data)] + [
            dcc.Store(id=id_store_writer) for id_store_writer in id_store_writers
        ]

    @app.callback(
        Output(id_container, "children"),
        [Input(id_store_writer, "modified_timestamp") for id_store_writer in id_store_writers],
        [State(id_store_writer, "data") for id_store_writer in id_store_writers]
        + [State(id_store_root, "data")],
    )
    def update_stores(*args):
        """Callback to handle the update of the root store based on the writer stores"""
        # process args to retrieve the different input/state components
        # extract data from root store
        *args, data_root = args
        # reorganise stores data as [(ts1, data1), ...]
        stores_info = list(zip(args[:N], args[N:]))
        # keep stores with timestamp defined and sort them by the timestamp
        stores_info = sorted(
            ((ts, data) for (ts, data) in stores_info if ts is not None), key=lambda item: item[0]
        )
        # print(stores_info)
        # print(dash.callback_context.triggered)
        if not stores_info:
            # no store updated
            return dash.no_update

        # in function of mode, update the data_root
        if mode == "replace":
            # replace the data_root by the more recent data store write
            ts, new_data = stores_info[-1]
            assert ts
            if new_data == data_root:
                return dash.no_update
            data_root = new_data

        else:
            # append to data_root all data store writes (chronologically)
            data_root.extend(data for ts, data in stores_info if ts)

        # rerender the layout with the new data
        layout = get_layout(data_root)

        return layout

    initial_data = None if mode == "replace" else []
    layout = html.Div(id=id_container, children=get_layout(initial_data))

    return layout


app.layout = html.Div(
    [
        dcc.Input(id="a", value="0", type="number"),
        dcc.Input(id="b", value="0", type="number"),
        dcc.Input(id="c", value="0", type="number"),
        html.Div(id="output-store"),
        get_multi_store("scenario", ["a", "b", "c"], mode="append"),
    ]
)


# display output store (for debugging purposes)
@app.callback(
    Output(component_id="output-store", component_property="children"),
    [Input(component_id="scenario", component_property="data")],
)
def display_store(c):
    return str(c)


# update a with b
@app.callback(
    Output(component_id="scenario-c", component_property="data"),
    [Input(component_id="c", component_property="value")],
)
def set_c(c):
    return {"c": c}


# update store with b
@app.callback(
    Output(component_id="scenario-b", component_property="data"),
    [Input(component_id="b", component_property="value")],
)
def set_b(b):
    return {"b": b}


# update store with a and update c (which triggers and update of store with c)
@app.callback(
    [
        Output(component_id="c", component_property="value"),
        Output(component_id="scenario-a", component_property="data"),
    ],
    [Input(component_id="a", component_property="value")],
)
def set_a_and_c(a):
    print(dash.callback_context.triggered)
    # prints [{'prop_id': '.', 'value': None}]
    return float(a) / 2, {"a": a}


if __name__ == "__main__":
    app.run_server(debug=True)
  • replace the result of pip list | grep dash below
dash                      1.18.1
dash-core-components      1.14.1
dash-html-components      1.1.1
dash-renderer             1.8.3
  • if frontend related, tell us your Browser, Version and OS

    • OS: windows 10
    • Browser chrome
    • Version: 87

Describe the bug

After the initialization of the app, the Store "scenario" holds the value:
[{'a': '0'}, {'b': '0'}, {'c': 0}]
When increasing the Input "a", the store changes to (as the input 'a' and the input 'c' are successively changed)
[{'a': '0'}, {'b': '0'}, {'c': 0}, {'a': 1}, {'c': 0.5}]
However, I see that the callback set_a_and_c is triggered once when the input 'a' is changed (OK) and twice afterwards with the line print(dash.callback_context.triggered) printing [{'prop_id': '.', 'value': None}]

Expected behavior

I would expect the callback set_a_and_c to be only triggered once with dash.callback_context.triggered==[{'prop_id': 'a.value', 'value': 1}].
Moreover, if I change the input 'c', I also see that the callback set_a_and_c is called twice with the dash.callback_context.triggered==[{'prop_id': 'a.value', 'value': 1}].

If I update the input 'b', then the callback set_a_and_c is called once with the dash.callback_context.triggered==[{'prop_id': 'a.value', 'value': 1}].

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3backlogbugsomething broken

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions