Skip to content

Commit 7961611

Browse files
authored
Merge pull request #1228 from plotly/prevent-initial-call
Prevent initial call
2 parents 152ce28 + fa6df3a commit 7961611

File tree

4 files changed

+383
-10
lines changed

4 files changed

+383
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## [UNRELEASED]
66
### Added
7+
- [#1228](https://github.com/plotly/dash/pull/1228) Adds control over firing callbacks on page (or layout chunk) load. Individual callbacks can have their initial calls disabled in their definition `@app.callback(..., prevent_initial_call=True)` and similar for `app.clientside_callback`. The app-wide default can also be changed with `app=Dash(prevent_initial_callbacks=True)`, then individual callbacks may disable this behavior.
78
- [#1201](https://github.com/plotly/dash/pull/1201) New attribute `app.validation_layout` allows you to create a multi-page app without `suppress_callback_exceptions=True` or layout function tricks. Set this to a component layout containing the superset of all IDs on all pages in your app.
89
- [#1078](https://github.com/plotly/dash/pull/1078) Permit usage of arbitrary file extensions for assets within component libraries
910

dash-renderer/src/actions/dependencies.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,6 +1238,9 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
12381238
) {
12391239
// This callback should trigger even with no changedProps,
12401240
// since the props that changed no longer exist.
1241+
// We're kind of abusing the `initialCall` flag here, it's
1242+
// more like a "final call" for the removed inputs, but
1243+
// this case is not subject to `prevent_initial_call`.
12411244
if (flatten(cb.getOutputs(newPaths)).length) {
12421245
cb.initialCall = true;
12431246
cb.changedPropIds = {};
@@ -1255,10 +1258,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
12551258
const cb = getCallbackByOutput(graphs, paths, id, property);
12561259
if (cb) {
12571260
// callbacks found in the layout by output should always run
1261+
// unless specifically requested not to.
12581262
// ie this is the initial call of this callback even if it's
12591263
// not the page initialization but just a new layout chunk
1260-
cb.initialCall = true;
1261-
addCallback(cb);
1264+
if (!cb.callback.prevent_initial_call) {
1265+
cb.initialCall = true;
1266+
addCallback(cb);
1267+
}
12621268
}
12631269
}
12641270
}
@@ -1270,14 +1276,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
12701276
if (chunkPath) {
12711277
handleThisCallback = cb => {
12721278
if (
1273-
all(
1279+
!all(
12741280
startsWith(chunkPath),
12751281
pluck('path', flatten(cb.getOutputs(paths)))
12761282
)
12771283
) {
1278-
cb.changedPropIds = {};
1284+
maybeAddCallback(cb);
12791285
}
1280-
maybeAddCallback(cb);
12811286
};
12821287
}
12831288
for (const property in inIdCallbacks) {

dash/dash.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,15 @@ class Dash(object):
211211
env: ``DASH_SUPPRESS_CALLBACK_EXCEPTIONS``
212212
:type suppress_callback_exceptions: boolean
213213
214+
:param prevent_initial_callbacks: Default ``False``: Sets the default value
215+
of ``prevent_initial_call`` for all callbacks added to the app.
216+
Normally all callbacks are fired when the associated outputs are first
217+
added to the page. You can disable this for individual callbacks by
218+
setting ``prevent_initial_call`` in their definitions, or set it
219+
``True`` here in which case you must explicitly set it ``False`` for
220+
those callbacks you wish to have an initial call. This setting has no
221+
effect on triggering callbacks when their inputs change later on.
222+
214223
:param show_undo_redo: Default ``False``, set to ``True`` to enable undo
215224
and redo buttons for stepping through the history of the app state.
216225
:type show_undo_redo: boolean
@@ -241,6 +250,7 @@ def __init__(
241250
external_scripts=None,
242251
external_stylesheets=None,
243252
suppress_callback_exceptions=None,
253+
prevent_initial_callbacks=False,
244254
show_undo_redo=False,
245255
plugins=None,
246256
**obsolete
@@ -288,6 +298,7 @@ def __init__(
288298
suppress_callback_exceptions=get_combined_config(
289299
"suppress_callback_exceptions", suppress_callback_exceptions, False
290300
),
301+
prevent_initial_callbacks=prevent_initial_callbacks,
291302
show_undo_redo=show_undo_redo,
292303
)
293304
self.config.set_read_only(
@@ -813,14 +824,18 @@ def interpolate_index(self, **kwargs):
813824
def dependencies(self):
814825
return flask.jsonify(self._callback_list)
815826

816-
def _insert_callback(self, output, inputs, state):
827+
def _insert_callback(self, output, inputs, state, prevent_initial_call):
828+
if prevent_initial_call is None:
829+
prevent_initial_call = self.config.prevent_initial_callbacks
830+
817831
_validate.validate_callback(output, inputs, state)
818832
callback_id = create_callback_id(output)
819833
callback_spec = {
820834
"output": callback_id,
821835
"inputs": [c.to_dict() for c in inputs],
822836
"state": [c.to_dict() for c in state],
823837
"clientside_function": None,
838+
"prevent_initial_call": prevent_initial_call,
824839
}
825840
self.callback_map[callback_id] = {
826841
"inputs": callback_spec["inputs"],
@@ -830,7 +845,9 @@ def _insert_callback(self, output, inputs, state):
830845

831846
return callback_id
832847

833-
def clientside_callback(self, clientside_function, output, inputs, state=()):
848+
def clientside_callback(
849+
self, clientside_function, output, inputs, state=(), prevent_initial_call=None
850+
):
834851
"""Create a callback that updates the output by calling a clientside
835852
(JavaScript) function instead of a Python function.
836853
@@ -890,8 +907,12 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
890907
Input('another-input', 'value')]
891908
)
892909
```
910+
911+
The last, optional argument `prevent_initial_call` causes the callback
912+
not to fire when its outputs are first added to the page. Defaults to
913+
`False` unless `prevent_initial_callbacks=True` at the app level.
893914
"""
894-
self._insert_callback(output, inputs, state)
915+
self._insert_callback(output, inputs, state, prevent_initial_call)
895916

896917
# If JS source is explicitly given, create a namespace and function
897918
# name, then inject the code.
@@ -922,8 +943,19 @@ def clientside_callback(self, clientside_function, output, inputs, state=()):
922943
"function_name": function_name,
923944
}
924945

925-
def callback(self, output, inputs, state=()):
926-
callback_id = self._insert_callback(output, inputs, state)
946+
def callback(self, output, inputs, state=(), prevent_initial_call=None):
947+
"""
948+
Normally used as a decorator, `@app.callback` provides a server-side
949+
callback relating the values of one or more `output` items to one or
950+
more `input` items which will trigger the callback when they change,
951+
and optionally `state` items which provide additional information but
952+
do not trigger the callback directly.
953+
954+
The last, optional argument `prevent_initial_call` causes the callback
955+
not to fire when its outputs are first added to the page. Defaults to
956+
`False` unless `prevent_initial_callbacks=True` at the app level.
957+
"""
958+
callback_id = self._insert_callback(output, inputs, state, prevent_initial_call)
927959
multi = isinstance(output, (list, tuple))
928960

929961
def wrap_func(func):

0 commit comments

Comments
 (0)