-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
setProps
and rendered
event ordering causing unsent callbacks
#1415
Changes from 2 commits
174252a
53f0bb2
958ead3
73304f8
6a65d2c
f20be46
d3dd668
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import PropTypes from 'prop-types'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding a set of components with simple behaviors corresponding to various usage scenarios. This component is async. |
||
import React, { Suspense } from 'react'; | ||
import { asyncDecorator } from '@plotly/dash-component-plugins'; | ||
import asyncComponentLoader from './../fragments/AsyncComponentLoader'; | ||
|
||
const AsyncComponent = props => (<Suspense fallback={null}> | ||
<RealAsyncComponent {...props} /> | ||
</Suspense>); | ||
|
||
const RealAsyncComponent = asyncDecorator(AsyncComponent, asyncComponentLoader); | ||
|
||
AsyncComponent.propTypes = { | ||
id: PropTypes.string, | ||
value: PropTypes.string | ||
}; | ||
|
||
AsyncComponent.defaultProps = {}; | ||
|
||
export default AsyncComponent; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import PropTypes from 'prop-types'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding a set of components with simple behaviors corresponding to various usage scenarios. This component delays |
||
import React from 'react'; | ||
|
||
const DelayedEventComponent = ({ id, n_clicks, setProps }) => (<button | ||
id={id} | ||
onClick={() => setTimeout(() => setProps({ n_clicks: n_clicks + 1 }), 20)} | ||
/>); | ||
|
||
DelayedEventComponent.propTypes = { | ||
id: PropTypes.string, | ||
n_clicks: PropTypes.number | ||
}; | ||
|
||
DelayedEventComponent.defaultProps = { | ||
n_clicks: 0 | ||
}; | ||
|
||
export default DelayedEventComponent; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import PropTypes from 'prop-types'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding a set of components with simple behaviors corresponding to various usage scenarios. This is a top-level component without |
||
import React, { Fragment } from 'react'; | ||
|
||
const FragmentComponent = props => (<Fragment> | ||
{props.children} | ||
</Fragment>); | ||
|
||
FragmentComponent.propTypes = { | ||
children: PropTypes.node, | ||
id: PropTypes.string | ||
}; | ||
|
||
FragmentComponent.defaultProps = {}; | ||
|
||
export default FragmentComponent; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import PropTypes from 'prop-types'; | ||
import React, { Fragment } from 'react'; | ||
import { asyncDecorator } from '@plotly/dash-component-plugins'; | ||
|
||
/** | ||
* MyComponent description | ||
*/ | ||
const AsyncComponent = ({ value }) => (<Fragment> | ||
{value} | ||
</Fragment>); | ||
|
||
AsyncComponent.propTypes = { | ||
id: PropTypes.string, | ||
value: PropTypes.string | ||
}; | ||
|
||
AsyncComponent.defaultProps = {}; | ||
|
||
export default AsyncComponent; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export default () => import('./../fragments/AsyncComponent'); | ||
Marc-Andre-Rivet marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,16 @@ | ||
import AsyncComponent from './components/AsyncComponent'; | ||
import DelayedEventComponent from './components/DelayedEventComponent'; | ||
import FragmentComponent from './components/FragmentComponent'; | ||
import StyledComponent from './components/StyledComponent'; | ||
import MyPersistedComponent from './components/MyPersistedComponent'; | ||
import MyPersistedComponentNested from './components/MyPersistedComponentNested'; | ||
|
||
|
||
export { | ||
StyledComponent, MyPersistedComponent, MyPersistedComponentNested | ||
AsyncComponent, | ||
DelayedEventComponent, | ||
FragmentComponent, | ||
MyPersistedComponent, | ||
MyPersistedComponentNested, | ||
StyledComponent | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -138,14 +138,6 @@ class BaseTreeContainer extends Component { | |
// for persistence | ||
recordUiEdit(_dashprivate_layout, newProps, _dashprivate_dispatch); | ||
|
||
// Always update this component's props | ||
_dashprivate_dispatch( | ||
updateProps({ | ||
props: changedProps, | ||
itempath: _dashprivate_path | ||
}) | ||
); | ||
|
||
// Only dispatch changes to Dash if a watched prop changed | ||
if (watchedKeys.length) { | ||
_dashprivate_dispatch( | ||
|
@@ -155,6 +147,14 @@ class BaseTreeContainer extends Component { | |
}) | ||
); | ||
} | ||
|
||
// Always update this component's props | ||
_dashprivate_dispatch( | ||
updateProps({ | ||
props: changedProps, | ||
itempath: _dashprivate_path | ||
}) | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Callbacks are processed after a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to make sure I'm understanding this correctly: Do I have that right? It still feels a bit fragile, like what we ideally would want to do is attach the event listener synchronously here somehow and just let it be used later, in case another There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes about the ordering change. TBH I have the same discomfort about the fragility or at least the fragmentation of the implementation between the APIController I've tested adding an additional |
||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,16 @@ | ||
import json | ||
from multiprocessing import Lock, Value | ||
|
||
import pytest | ||
|
||
import dash_core_components as dcc | ||
import dash_html_components as html | ||
import dash_table | ||
import dash | ||
from dash_test_components import ( | ||
AsyncComponent, | ||
DelayedEventComponent, | ||
FragmentComponent, | ||
) | ||
from dash.dependencies import Input, Output, State | ||
from dash.exceptions import PreventUpdate | ||
from dash.testing import wait | ||
|
@@ -404,3 +408,53 @@ def update_text(data): | |
assert input_call_count.value == 2 + len("hello world") | ||
|
||
assert not dash_duo.get_logs() | ||
|
||
|
||
def test_cbsc009_callback_using_unloaded_async_component_and_graph(dash_duo): | ||
app = dash.Dash(__name__) | ||
app.layout = FragmentComponent( | ||
children=[ | ||
dcc.Tabs( | ||
value="1", | ||
children=[ | ||
dcc.Tab(label="Tab 1", value="1", children="Tab 1 Content"), | ||
dcc.Tab( | ||
label="2", | ||
value="2", | ||
children=AsyncComponent(id="async", value="A"), | ||
), | ||
], | ||
), | ||
html.Button("n", id="n"), | ||
DelayedEventComponent(id="d"), | ||
html.Div("Output init", id="output"), | ||
] | ||
) | ||
|
||
@app.callback( | ||
Output("output", "children"), | ||
Input("n", "n_clicks"), | ||
Input("d", "n_clicks"), | ||
Input("async", "value"), | ||
) | ||
def content(n, d, v): | ||
return json.dumps([n, d, v]) | ||
|
||
dash_duo.start_server(app) | ||
|
||
wait.until(lambda: dash_duo.find_element("#output").text == '[null, null, "A"]', 3) | ||
dash_duo.wait_for_element("#d").click() | ||
|
||
wait.until( | ||
lambda: dash_duo.find_element("#output").text == '[null, 1, "A"]', 3, | ||
) | ||
|
||
dash_duo.wait_for_element("#n").click() | ||
wait.until( | ||
lambda: dash_duo.find_element("#output").text == '[1, 1, "A"]', 3, | ||
) | ||
|
||
dash_duo.wait_for_element("#d").click() | ||
wait.until( | ||
lambda: dash_duo.find_element("#output").text == '[1, 2, "A"]', 3, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Simplified version of the original reproducible example involving the DataTable inside a dcc.Tab and the dcc.Graph, all inside a non-click reactive top-level component -- does not do away with all real components as it would be tedious but used custom made components for the key behavior. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test was run and fails against |
||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add async support for the test components