Repository: plotly/dash

Issue:
Ability to prevent initial callback from firing
@chriddyp started prototyping this in #1123 and I'm preparing a new PR that will supersede #1123 with a more complete implementation and tests. But a question about how this should work regarding later callbacks:

Currently if all of the inputs to callback C are themselves outputs to other callbacks A and B, we effectively do not treat C as an "initial callback" - that is, it won't trigger without one of its inputs changing, so if A and B both raise `PreventUpdate` on page load (or layout chunk load), C will not be called.

The question is how this should apply to `prevent_initial_call` - if A and B both have `prevent_initial_call=True`, is that equivalent to `PreventUpdate` so C should not fire? Or is it as though A and B are simply not considered initially and the initialization chain should start at C?

My initial reaction is that this case is not the same as `PreventUpdate` and C **should** fire. My reasoning:
- Presumably in this situation you've set the initial values for A and B as part of the layout - so it's not so much that you're preventing an update, it's just that for performance reasons you've already made the update happen.
- If you do want C to fire and we automatically prevent it, you'd have to do something super hacky like add an extra input. On the other hand, if we allow it to fire and you don't want that, all you have to do is set `prevent_initial_call=True` on C as well.

Anyone care to argue the other side? @Marc-Andre-Rivet @chriddyp @christianwengert do you agree?


Discussion:
@alexcjohnson Initial reaction is also that `prevent_initial_callback` != `PreventUpdate`. By setting `prevent_initial_callback=True` you are excluding these callbacks from the initial callback graph and effectively making follow up callbacks the initial ones.

As we want to conserve backward compatibility, we will want `prevent_initial_callback=False` to be the default. This raises the issue that once you set `True` for one callback it might be necessary for the app developer to set all other callbacks to `True` explicitly to avoid the runaway behavior described above. Would it be possible to add a global `prevent_initial_callback` as part of this PR. The callback parameter would then serve to override the global/default one as needed. Allowing full flexibility / minimal code changes from the app developer in all scenarios.
> As we want to conserve backward compatibility, we will want `prevent_initial_callback=False` to be the default.

Absolutely.

> Would it be possible to add a global `prevent_initial_callback` as part of this PR. The callback parameter would then serve to override the global/default one as needed.

That's a good idea, should be an easy add. I'd call the global one `prevent_intitial_callbacks` though.
>If you do want C to fire and we automatically prevent it, you'd have to do something super hacky like add an extra input. On the other hand, if we allow it to fire and you don't want that, all you have to do is set prevent_initial_call=True on C as well.

Personally I also prefer that C should fire -- I can imagine why an app developer would want to take advantage of this, and if I understand correctly, this seems to be what a more experienced user of Dash _would expect_ (outside of suspending initial execution of A and B) under the circumstances.
:+1: That reasoning is sound to me
I also agree, I would expect C to fire unless explicitly silenced. I also think that this is a more advanced dash feature. 
My use case did not involve callback chains so far, but was to silence callbacks for dynamic inputs (pattern matching) for performance reasons, e.g. if you dynamically add many inputs (several dozens) to the layout the app gets unresponsive for a few seconds because all callbacks are called.


Resolving patch:
diff --git a/dash/dash.py b/dash/dash.py
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -211,6 +211,15 @@ class Dash(object):
         env: ``DASH_SUPPRESS_CALLBACK_EXCEPTIONS``
     :type suppress_callback_exceptions: boolean
 
+    :param prevent_initial_callbacks: Default ``False``: Sets the default value
+        of ``prevent_initial_call`` for all callbacks added to the app.
+        Normally all callbacks are fired when the associated outputs are first
+        added to the page. You can disable this for individual callbacks by
+        setting ``prevent_initial_call`` in their definitions, or set it
+        ``True`` here in which case you must explicitly set it ``False`` for
+        those callbacks you wish to have an initial call. This setting has no
+        effect on triggering callbacks when their inputs change later on.
+
     :param show_undo_redo: Default ``False``, set to ``True`` to enable undo
         and redo buttons for stepping through the history of the app state.
     :type show_undo_redo: boolean
@@ -241,6 +250,7 @@ def __init__(
         external_scripts=None,
         external_stylesheets=None,
         suppress_callback_exceptions=None,
+        prevent_initial_callbacks=False,
         show_undo_redo=False,
         plugins=None,
         **obsolete
@@ -288,6 +298,7 @@ def __init__(
             suppress_callback_exceptions=get_combined_config(
                 "suppress_callback_exceptions", suppress_callback_exceptions, False
             ),
+            prevent_initial_callbacks=prevent_initial_callbacks,
             show_undo_redo=show_undo_redo,
         )
         self.config.set_read_only(
@@ -813,7 +824,10 @@ def interpolate_index(self, **kwargs):
     def dependencies(self):
         return flask.jsonify(self._callback_list)
 
-    def _insert_callback(self, output, inputs, state):
+    def _insert_callback(self, output, inputs, state, prevent_initial_call):
+        if prevent_initial_call is None:
+            prevent_initial_call = self.config.prevent_initial_callbacks
+
         _validate.validate_callback(output, inputs, state)
         callback_id = create_callback_id(output)
         callback_spec = {
@@ -821,6 +835,7 @@ def _insert_callback(self, output, inputs, state):
             "inputs": [c.to_dict() for c in inputs],
             "state": [c.to_dict() for c in state],
             "clientside_function": None,
+            "prevent_initial_call": prevent_initial_call,
         }
         self.callback_map[callback_id] = {
             "inputs": callback_spec["inputs"],
@@ -830,7 +845,9 @@ def _insert_callback(self, output, inputs, state):
 
         return callback_id
 
-    def clientside_callback(self, clientside_function, output, inputs, state=()):
+    def clientside_callback(
+        self, clientside_function, output, inputs, state=(), prevent_initial_call=None
+    ):
         """Create a callback that updates the output by calling a clientside
         (JavaScript) function instead of a Python function.
 
@@ -890,8 +907,12 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
              Input('another-input', 'value')]
         )
         ```
+
+        The last, optional argument `prevent_initial_call` causes the callback
+        not to fire when its outputs are first added to the page. Defaults to
+        `False` unless `prevent_initial_callbacks=True` at the app level.
         """
-        self._insert_callback(output, inputs, state)
+        self._insert_callback(output, inputs, state, prevent_initial_call)
 
         # If JS source is explicitly given, create a namespace and function
         # name, then inject the code.
@@ -922,8 +943,19 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
             "function_name": function_name,
         }
 
-    def callback(self, output, inputs, state=()):
-        callback_id = self._insert_callback(output, inputs, state)
+    def callback(self, output, inputs, state=(), prevent_initial_call=None):
+        """
+        Normally used as a decorator, `@app.callback` provides a server-side
+        callback relating the values of one or more `output` items to one or
+        more `input` items which will trigger the callback when they change,
+        and optionally `state` items which provide additional information but
+        do not trigger the callback directly.
+
+        The last, optional argument `prevent_initial_call` causes the callback
+        not to fire when its outputs are first added to the page. Defaults to
+        `False` unless `prevent_initial_callbacks=True` at the app level.
+        """
+        callback_id = self._insert_callback(output, inputs, state, prevent_initial_call)
         multi = isinstance(output, (list, tuple))
 
         def wrap_func(func):


Test patch:
diff --git a/tests/integration/callbacks/test_prevent_initial.py b/tests/integration/callbacks/test_prevent_initial.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/callbacks/test_prevent_initial.py
@@ -0,0 +1,335 @@
+import json
+import pytest
+
+import dash_html_components as html
+import dash
+from dash.dependencies import Input, Output, MATCH
+from dash.exceptions import PreventUpdate
+
+# parametrize all the tests, but no need to do all 8 combinations, 5 is enough!
+flavors = [
+    {"clientside": False, "content": False, "global": False},
+    {"clientside": True, "content": False, "global": False},
+    {"clientside": False, "content": True, "global": False},
+    {"clientside": True, "content": True, "global": True},
+    {"clientside": False, "content": False, "global": True},
+]
+
+
+def make_app(flavor):
+    kwargs = {}
+    if flavor["global"]:
+        kwargs["prevent_initial_callbacks"] = True
+    return dash.Dash(__name__, **kwargs)
+
+
+def content_callback(app, flavor, layout):
+    kwargs = {}
+    if flavor["global"]:
+        kwargs["prevent_initial_call"] = False
+
+    if flavor["content"]:
+        app.layout = html.Div(id="content")
+
+        @app.callback(
+            Output("content", "children"), [Input("content", "style")], **kwargs
+        )
+        def set_content(_):
+            return layout
+
+    else:
+        app.layout = layout
+
+
+def const_callback(app, flavor, val, outputs, inputs, prevent_initial_call=False):
+    kwargs = {}
+    if prevent_initial_call != flavor["global"]:
+        kwargs["prevent_initial_call"] = prevent_initial_call
+
+    if flavor["clientside"]:
+        vstr = json.dumps(val)
+        app.clientside_callback(
+            "function() { return " + vstr + "; }", outputs, inputs, **kwargs
+        )
+    else:
+
+        @app.callback(outputs, inputs, **kwargs)
+        def f(*args):
+            return val
+
+
+def concat_callback(app, flavor, outputs, inputs, prevent_initial_call=False):
+    kwargs = {}
+    if prevent_initial_call != flavor["global"]:
+        kwargs["prevent_initial_call"] = prevent_initial_call
+
+    multi_out = isinstance(outputs, (list, tuple))
+
+    if flavor["clientside"]:
+        app.clientside_callback(
+            """
+            function() {
+                var out = '';
+                for(var i = 0; i < arguments.length; i++) {
+                    out += String(arguments[i]);
+                }
+                return X;
+            }
+            """.replace(
+                "X",
+                ("[" + ",".join(["out"] * len(outputs)) + "]") if multi_out else "out",
+            ),
+            outputs,
+            inputs,
+            **kwargs
+        )
+    else:
+
+        @app.callback(outputs, inputs, **kwargs)
+        def f(*args):
+            out = "".join(str(arg) for arg in args)
+            return [out] * len(outputs) if multi_out else out
+
+
+@pytest.mark.parametrize("flavor", flavors)
+def test_cbpi001_prevent_initial_call(flavor, dash_duo):
+    app = make_app(flavor)
+
+    layout = html.Div(
+        [
+            html.Button("click", id="btn"),
+            html.Div("A", id="a"),
+            html.Div("B", id="b"),
+            html.Div("C", id="c"),
+            html.Div("D", id="d"),
+            html.Div("E", id="e"),
+            html.Div("F", id="f"),
+        ]
+    )
+    content_callback(app, flavor, layout)
+
+    # Prevented, so A will only change after the button is clicked
+    const_callback(
+        app,
+        flavor,
+        "Click",
+        Output("a", "children"),
+        [Input("btn", "n_clicks")],
+        prevent_initial_call=True,
+    )
+
+    # B depends on A - this *will* run, because prevent_initial_call is
+    # not equivalent to PreventUpdate within the callback, it's treated as if
+    # that callback was never in the initialization chain.
+    concat_callback(app, flavor, Output("b", "children"), [Input("a", "children")])
+
+    # C matches B except that it also has prevent_initial_call itself, not just
+    # its input A - so it will not run initially
+    concat_callback(
+        app,
+        flavor,
+        Output("c", "children"),
+        [Input("a", "children")],
+        prevent_initial_call=True,
+    )
+
+    @app.callback(Output("d", "children"), [Input("d", "style")])
+    def d(_):
+        raise PreventUpdate
+
+    # E depends on A and D - one prevent_initial_call and one PreventUpdate
+    # the prevent_initial_call means it *will* run
+    concat_callback(
+        app,
+        flavor,
+        Output("e", "children"),
+        [Input("a", "children"), Input("d", "children")],
+    )
+
+    # F has prevent_initial_call but DOES fire during init, because one of its
+    # inputs (B) was changed by a callback
+    concat_callback(
+        app,
+        flavor,
+        Output("f", "children"),
+        [Input("a", "children"), Input("b", "children"), Input("d", "children")],
+        prevent_initial_call=True,
+    )
+
+    dash_duo.start_server(app)
+
+    # check from the end, to ensure the callbacks are all done
+    dash_duo.wait_for_text_to_equal("#f", "AAD")
+    dash_duo.wait_for_text_to_equal("#e", "AD"),
+    dash_duo.wait_for_text_to_equal("#d", "D")
+    dash_duo.wait_for_text_to_equal("#c", "C")
+    dash_duo.wait_for_text_to_equal("#b", "A")
+    dash_duo.wait_for_text_to_equal("#a", "A")
+
+    dash_duo.find_element("#btn").click()
+
+    dash_duo.wait_for_text_to_equal("#f", "ClickClickD")
+    dash_duo.wait_for_text_to_equal("#e", "ClickD"),
+    dash_duo.wait_for_text_to_equal("#d", "D")
+    dash_duo.wait_for_text_to_equal("#c", "Click")
+    dash_duo.wait_for_text_to_equal("#b", "Click")
+    dash_duo.wait_for_text_to_equal("#a", "Click")
+
+
+@pytest.mark.parametrize("flavor", flavors)
+def test_cbpi002_pattern_matching(flavor, dash_duo):
+    # a clone of cbpi001 just throwing it through the pattern-matching machinery
+    app = make_app(flavor)
+
+    layout = html.Div(
+        [
+            html.Button("click", id={"i": 0, "j": "btn"}, className="btn"),
+            html.Div("A", id={"i": 0, "j": "a"}, className="a"),
+            html.Div("B", id={"i": 0, "j": "b"}, className="b"),
+            html.Div("C", id={"i": 0, "j": "c"}, className="c"),
+            html.Div("D", id={"i": 0, "j": "d"}, className="d"),
+            html.Div("E", id={"i": 0, "j": "e"}, className="e"),
+            html.Div("F", id={"i": 0, "j": "f"}, className="f"),
+        ]
+    )
+    content_callback(app, flavor, layout)
+
+    const_callback(
+        app,
+        flavor,
+        "Click",
+        Output({"i": MATCH, "j": "a"}, "children"),
+        [Input({"i": MATCH, "j": "btn"}, "n_clicks")],
+        prevent_initial_call=True,
+    )
+
+    concat_callback(
+        app,
+        flavor,
+        Output({"i": MATCH, "j": "b"}, "children"),
+        [Input({"i": MATCH, "j": "a"}, "children")],
+    )
+
+    concat_callback(
+        app,
+        flavor,
+        Output({"i": MATCH, "j": "c"}, "children"),
+        [Input({"i": MATCH, "j": "a"}, "children")],
+        prevent_initial_call=True,
+    )
+
+    @app.callback(
+        Output({"i": MATCH, "j": "d"}, "children"),
+        [Input({"i": MATCH, "j": "d"}, "style")],
+    )
+    def d(_):
+        raise PreventUpdate
+
+    concat_callback(
+        app,
+        flavor,
+        Output({"i": MATCH, "j": "e"}, "children"),
+        [
+            Input({"i": MATCH, "j": "a"}, "children"),
+            Input({"i": MATCH, "j": "d"}, "children"),
+        ],
+    )
+
+    concat_callback(
+        app,
+        flavor,
+        Output({"i": MATCH, "j": "f"}, "children"),
+        [
+            Input({"i": MATCH, "j": "a"}, "children"),
+            Input({"i": MATCH, "j": "b"}, "children"),
+            Input({"i": MATCH, "j": "d"}, "children"),
+        ],
+        prevent_initial_call=True,
+    )
+
+    dash_duo.start_server(app)
+
+    # check from the end, to ensure the callbacks are all done
+    dash_duo.wait_for_text_to_equal(".f", "AAD")
+    dash_duo.wait_for_text_to_equal(".e", "AD"),
+    dash_duo.wait_for_text_to_equal(".d", "D")
+    dash_duo.wait_for_text_to_equal(".c", "C")
+    dash_duo.wait_for_text_to_equal(".b", "A")
+    dash_duo.wait_for_text_to_equal(".a", "A")
+
+    dash_duo.find_element(".btn").click()
+
+    dash_duo.wait_for_text_to_equal(".f", "ClickClickD")
+    dash_duo.wait_for_text_to_equal(".e", "ClickD"),
+    dash_duo.wait_for_text_to_equal(".d", "D")
+    dash_duo.wait_for_text_to_equal(".c", "Click")
+    dash_duo.wait_for_text_to_equal(".b", "Click")
+    dash_duo.wait_for_text_to_equal(".a", "Click")
+
+
+@pytest.mark.parametrize("flavor", flavors)
+def test_cbpi003_multi_outputs(flavor, dash_duo):
+    app = make_app(flavor)
+
+    layout = html.Div(
+        [
+            html.Button("click", id="btn"),
+            html.Div("A", id="a"),
+            html.Div("B", id="b"),
+            html.Div("C", id="c"),
+            html.Div("D", id="d"),
+            html.Div("E", id="e"),
+            html.Div("F", id="f"),
+            html.Div("G", id="g"),
+        ]
+    )
+
+    content_callback(app, flavor, layout)
+
+    const_callback(
+        app,
+        flavor,
+        ["Blue", "Cheese"],
+        [Output("a", "children"), Output("b", "children")],
+        [Input("btn", "n_clicks")],
+        prevent_initial_call=True,
+    )
+
+    concat_callback(
+        app,
+        flavor,
+        [Output("c", "children"), Output("d", "children")],
+        [Input("a", "children"), Input("b", "children")],
+        prevent_initial_call=True,
+    )
+
+    concat_callback(
+        app,
+        flavor,
+        [Output("e", "children"), Output("f", "children")],
+        [Input("a", "children")],
+        prevent_initial_call=True,
+    )
+
+    # this is the only one that should run initially
+    concat_callback(app, flavor, Output("g", "children"), [Input("f", "children")])
+
+    dash_duo.start_server(app)
+
+    dash_duo.wait_for_text_to_equal("#g", "F")
+    dash_duo.wait_for_text_to_equal("#f", "F")
+    dash_duo.wait_for_text_to_equal("#e", "E")
+    dash_duo.wait_for_text_to_equal("#d", "D")
+    dash_duo.wait_for_text_to_equal("#c", "C")
+    dash_duo.wait_for_text_to_equal("#b", "B")
+    dash_duo.wait_for_text_to_equal("#a", "A")
+
+    dash_duo.find_element("#btn").click()
+
+    dash_duo.wait_for_text_to_equal("#g", "Blue")
+    dash_duo.wait_for_text_to_equal("#f", "Blue")
+    dash_duo.wait_for_text_to_equal("#e", "Blue")
+    dash_duo.wait_for_text_to_equal("#d", "BlueCheese")
+    dash_duo.wait_for_text_to_equal("#c", "BlueCheese")
+    dash_duo.wait_for_text_to_equal("#b", "Cheese")
+    dash_duo.wait_for_text_to_equal("#a", "Blue")