-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Callback initialization with None
vs default properties
#468
Comments
cc @plotly/dash |
In Solution 1, if we had a callback with 4 inputs where 1 is provided in the initial layout and 3 are not, would it fire? If so, would the values of those 3 be |
If a callback has no Input (nor State), will it be fired? But perhaps easier to have a custom object for such use case to initialize a value within a Flask context, something like: dcc.Input(id='input-b', value=dash.FlaskContext(init_input), with def init_input():
from Flask import request, g
return request.headers.get("X-DEFAULT-VALUE", "no default provided") instead of a the potential callback solution (if it gets fired at app startup time) @app.callback(
Output('input-b', 'value'),
)
def init_input(v):
from Flask import request, g
return request.headers.get("X-DEFAULT-VALUE", "no default provided") |
What I ended up using was a simple work-around within the decorated callback (so simple, it's probably been done before but I thought I'd share anyway). If you mask the input variables hitting the function such that they can be checked with a simple conditional - similar to how @chriddyp proposed but you can chain them all into the first
Other than that, I'd vote for a combination of Solutions 1 & 2 with the thinking that if you're able to set the callbacks to be/not-be self-triggering, it would be a similar mechanism to allow a default value setting. |
Long obsolete, with |
plotly/dash-renderer#81 is a proposal to change our Dash callbacks are fired upon initialization. In particular, it prevents the initial callbacks from firing if properties weren't explicitly provided.
Through this analysis, I've come to the conclusion that plotly/dash-renderer#81 isn't a complete solution to the underlying issues and inconsistencies. I propose a couple of alternative solutions towards the end. These solutions will require a larger engineering effort and design discussion and so may not be solved on the timeline of our 1.0.0 breaking change release.
Technical Background
Currently, when Dash apps load, Dash fires a certain set of callbacks:
If a callback's property wasn't defined in the initial
app.layout
, then it is supplied in thecallback
asNone
. If it was supplied, then it is provided in thecallback
as that value.For example:
In this example, Dash will:
update_output_1
withNone
update_output_2
with'initial value 2'
update_output_c
withNone
and'initial value 2'
update_output_3
with'output 2 update'
update_output_3a
with'output 1 update'
andNone
update_output_3b
with'output 1 update'
and'initial value 2'
The proposed change would change Dash so that callbacks with any inputs with properties that aren't supplied aren't called on initialization.
With this change, the following callbacks would be fired on page load:
update_2
with'initial value 2'
And after
output-2
is updated, the following callbacks are triggered:update_output_3
with'output 2 update'
update_output_3b
with'output 1 update'
and'initial value 2'
Historical Context
Why are callbacks fired on page load?
Callbacks are fired on page load for consistency. Consider the following case:
In the current version of Dash, the
figure
property of thedcc.Graph
goes through the following transitions:None
update_graph
), updatingfigure
to{'layout': {'title': 'NYC'}}
After initialization, the app is in the same state on page load as it would be if the
dcc.Input
was empty and then the user wrote'NYC'
. In other words, the app's initializaiton state is no different than any other state in the application: the app is completely described by the values of the input properties and not by its history or previous state.If we didn't fire the callbacks on page load, then the
figure
would remain asNone
but the input would be filled with'NYC'
. If the user deleted the text and then re-wrote'NYC'
in the input, then the graph would have{'layout': {'title': 'NYC'}}
, which would appear odd as this is not what the graph looked like in the original state of the app even though thedcc.Input
was in the same state. This is what we mean when we say that this is "inconsistent".plotly/dash-renderer#81 proposes changing the behaviour when
value
in thedcc.Input
isn't supplied. Consider this example:plotly/dash-renderer#81 proposes that the
update_graph
callback should not be fired as thevalue
property of thedcc.Input
was not provided.In the current version of Dash, if a property isn't explicitly supplied then it is passed into the callback as
None
. From the Dash user's perspective, it's the same asdcc.Input(id='input', value=None)
.Inconsistencies with the existing behavior
Passing undefined properties as
None
into the callbacks is actually a little problematic:Empty Value. In many components,
None
isn't actually the property's "empty" value as it's not a state that can be achieved through user interaction. Consider the following components:dcc.Input(type='text')
- The empty state forvalue
is''
. That is, if the user deletes the text in the input box,''
is passed back to the callback, notNone
(type='number'
may have a different "empty" value, perhapsNone
)dcc.Dropdown(multi=True)
-value
is[]
when there aren't any items in the dropdown, notNone
. However, ifmulti=False
, thenNone
is the valid empty state.This means that the Dash developer has to handle two different "empty" states of the property with logic like:
None can be invalid. Since
None
isn't provided by the component, it isn't necessarily valid. This is non-intuitive becauseNone
was supplied to the callback from Dash and so it is assumed that it is the actual value of the property. This assumption would lead the user to believe that:and
would render the same result. They might, but it's not guaranteed. It's up to the component author to enforce this.
From a component author's perspective, these two initializations are different. In the former,
figure
is supplied and its explicitly set toNone
. In the latter,figure
isn't even supplied. The component author could render the component in different ways depending on which value was supplied. Handling these differences is most often done viadefaultProps
(see next point).Default Value. When components are rendered in the front-end, the component author may provide default properties for the component (using e.g. React's standard
defaultProps
) if the properties aren't supplied.For example, consider
dcc.Graph
: thefigure
component could have the following states:None
, then don't draw anything, not even an empty graph.{'data': [], 'layout': {}}
. This will draw an empty graph.In this case,
dcc.Graph()
would default to something reasonable (an empty graph) but if the user really wanted to clear the container they could setdcc.Graph(figure=None)
.However, if the user had a callback listening to the
figure
property, then they would receiveNone
as an argument on initialization, which doesn't match what was rendered in the front-end ({'data': [], 'layout': {}}
).dcc.Graph()
would be rendered differently thandcc.Graph(figure=None)
but in both casesNone
would be passed into the callback:Similarly, consider
n_clicks
inhtml.Button
.n_clicks
represents the number of times that the element has been clicked and so it's intuitive that its default value is0
(and it is). However, since Dash doesn't send this value to the callback,None
is passed into the callback, causing a lot of confusion.Computed Defaults. Some components have "computed" defaults. That is, their default properties can't be statically defined. For example:
dcc.Graph.id
- Computed dynamically (a random string)dcc.Location.path
- Thehref
property (among others indcc.Location
) isn't known until the component has rendered on the page as the user could be loading the page on any path, e.g./
or/some-page
dcc.Store.data
- If theStore
has data stored in local storage, then it must dynamically retrieve this data on page load.dash_table.DataTable.derived_virtual_data
- This property represents the data in a table after it has been filtered or sorted. Currently, it is not computed on initialization, but it should be. See the example here: https://dash.plot.ly/datatable/interactivityThese dynamic defaults cause confusing initialization behavior. Consider the following example:
On page load,
update_output
is initially called withNone
and then immediately called with the actualpath
. In this case,None
doesn't actually represent the "empty" state, it's more like the "unknown" state or the "uncomputed" state.So, to handle this use case, users should write their code as:
With static defaults, the user could always remove the
None
checks by supplying their own initial value. For example, instead ofdcc.Input()
they could writedcc.Input(value='')
. In the case of computed defaults, the user can't do this as they don't know what the properties will be. They can't writedcc.Location(path='/')
because the the user won't necessarily land on/
when they visit the page, they may land on/page-1
.Possible Solutions
Solution 1. Don't fire initial callbacks if inputs aren't supplied
dash-renderer
to skip firing the initial callbacks if the input properties aren't explicitly suppliedsetProps
in the component lifecycle. This would allow components with computed defaults to fire their initial callbacks (e.g.dcc.Storage.data
ordcc.Location.path
)State
would be passed into callbacks as their default (computed or static) properties instead ofNone
Notes:
By omitting an initial property value, users could prevent the initial request from occurring. Think of this as an "automatic", front-end version of
raise dash.exceptions.PreventDefault
.Components with dynamic properties would be responsible for firing the callbacks after initialization. This means that the "consistency" of whether or not callbacks are fired upon initialization is determined by the component author on a component-by-component basis.
This could be confusing as dash devs would not know the initialization behaviour in advance. This may be difficult to explain to new users. Here's an example of what this documentation might look like:
This could improve initial page render performance time as fewer requests could be made.
Users would be encouraged to supply initial values of their inputs so that the initial state of the app's lifecycle would be "consistent" and so that the outputs would have default values. The main exceptions to this rule would be
n_clicks
(users could omit this so that the callback wouldn't be fired until the user clicks on the button) and the computed properties (which can't be supplied by the user).This method would allow certain initial requests to be ignored but not all of them. This may be confusing to users: they may expect that they could programatically ignore an initial callback that has
derived_virtual_data
as anInput
by just not supplying an initial value for that property. However, since it is computed, they can't ignore this callback.The default properties (computed or static) would need to be provided as
State
. In the following example,derived_virtual_data
would be equal todata
andfigure
would be something like{'data': [], 'layout': {}}
If the property was supplied explicitly as
None
, the callback would still be fired.dcc.Dropdown(value=None)
would fire the callback butdcc.Dropdown()
would not fire the callback.Solution 2. Fire callbacks with default (computed or static) properties
This solution would fire all of the callbacks on initialization but instead of passing in undefined properties as
None
, it would use the component's static or computed default properties.Notes:
If the property doesn't have a default value, it would be
None
. This would be the same as if the component had a default value and it was explicitlyNone
.Undefined
", closer matching the JavaScript API.The mechanism for retrieving the default properties from the component would be the same mechanism that "Solution 1" would use to retrieve the properties to populate
State
This behaviour is easier to explain:
Certain components frequently would have
PreventDefault
in their canonical usage, likehtml.Button
:or, users would supply some default output properties:
Since we would start passing default props back to the callbacks, the component's default set of properties would become part of the official component API. That is, we wouldn't be able to change these properties without it being considered a breaking change. So, if we go forward with one of these solutions, we should inspect the default properties for all of our components.
Architecture Discussions
Mechanisms to retrieve the default properties
In previous prototypes (#288), we've incorporated the serialized static prop types (serialized as part of the
metadata.json
files created withreact-docgen
) into the dynamic python classes. This solution required no changes todash-renderer
as it would include the default properties implicitly in the serialization. That is,dcc.Dropdown()
would get serialized the same way as if it was specifieddcc.Dropdown(multi=False, value=None, options=[])
. This solution reduces the complexity indash-renderer
but it has a few flaws:html
components have 10s of properties and this could add up for large layouts.For similar reasons, plotly/dash-renderer#81 isn't a complete solution either. While its part of "Solution 1" above, it doesn't handle passing comptued data into the callback as
State
. In this solution, the components provide the computed defaults on their own schedule (by callingsetProps
in their component lifecycle, frequently oncomponentDidMount
). This means that the initialization callbacks won't necessarily have these computed values, meaning thatNone
would still be passed in asState
for unspecified properties.In order to handle computed defaults, we'll need a solution that works in
dash-renderer
. Here's a basic sketch of the solution:(This is the current behaviour) The dev-supplied component tree is serialized in the
layout
redux store.TreeContainer.react
recursively crawls thislayout
object (as it does now). At each node, it would:a. Determine which component properties are
Input
orState
.b. For those properties, retrieve the default properties of that component and merges them into the that node in the
layout
store. These default properties are static or are a function of the dev-supplied properties.c. Render the component with this new set of properties.
Alternatively, b and c could be reversed:
dash-renderer
could render the component (letting the component itself work out its default properties) and then extract the properties from the component.I have not investigated if there is a standard way to retrieve the default props from a component. In an ideal world,
dash-renderer
would be able to introspectdefaultProps
or callgetDefaultProps
. I suspect that this isn't possible without instantiating the component first. If so, we would need to extract the props after instantiating the component and we'd need to ensure that the component's default properties have been computed at this point. For example, if a component computes its default props incomponentDidMount
, this will not be called until the component is rendered (which, at this point in the app's lifecycle, the component is only instantiated, it hasn't been rendered yet).If it's not possible to extract these properties via the React component classes/instances, then we could define a new Dash-specific component class method like
computeDefaultProps(props)
and call that before rendering. This would increase the component authoring complexity and so it would be preferable if we could avoid this.The text was updated successfully, but these errors were encountered: