Skip to content

Commit d556791

Browse files
authored
Merge pull request #3395 from plotly/fix/set-props-paths
Fix set_props with children not having prop value in callbacks
2 parents 39a002f + 46a6a85 commit d556791

File tree

4 files changed

+92
-6
lines changed

4 files changed

+92
-6
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5+
## [UNRELEASED]
6+
7+
## Fixed
8+
- [#3395](https://github.com/plotly/dash/pull/3395) Fix Components added through set_props() cannot trigger related callback functions. Fix [#3316](https://github.com/plotly/dash/issues/3316)
9+
510
## [3.2.0] - 2025-07-31
611

712
## Added

dash/dash-renderer/src/actions/callbacks.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@ import {
3636
} from '../types/callbacks';
3737
import {isMultiValued, stringifyId, isMultiOutputProp} from './dependencies';
3838
import {urlBase} from './utils';
39-
import {getCSRFHeader, dispatchError} from '.';
39+
import {getCSRFHeader, dispatchError, setPaths} from '.';
4040
import {createAction, Action} from 'redux-actions';
4141
import {addHttpHeaders} from '../actions';
4242
import {notifyObservers, updateProps} from './index';
4343
import {CallbackJobPayload} from '../reducers/callbackJobs';
4444
import {handlePatch, isPatch} from './patch';
45-
import {getPath} from './paths';
45+
import {computePaths, getPath} from './paths';
4646

4747
import {requestDependencies} from './requestDependencies';
4848

@@ -51,6 +51,7 @@ import {loadLibrary} from '../utils/libraries';
5151
import {parsePMCId} from './patternMatching';
5252
import {replacePMC} from './patternMatching';
5353
import {loaded, loading} from './loading';
54+
import {getComponentLayout} from '../wrapper/wrapping';
5455

5556
export const addBlockedCallbacks = createAction<IBlockedCallback[]>(
5657
CallbackActionType.AddBlocked
@@ -409,7 +410,30 @@ function sideUpdate(outputs: SideUpdateOutput, cb: ICallbackPayload) {
409410
return acc;
410411
}, [] as any[])
411412
.forEach(([id, idProps]) => {
413+
const state = getState();
412414
dispatch(updateComponent(id, idProps, cb));
415+
416+
const componentPath = getPath(state.paths, id);
417+
if (!componentPath) {
418+
// Component doesn't exist, doesn't matter just allow the
419+
// callback to continue.
420+
return;
421+
}
422+
const oldComponent = getComponentLayout(componentPath, state);
423+
424+
dispatch(
425+
setPaths(
426+
computePaths(
427+
{
428+
...oldComponent,
429+
props: {...oldComponent.props, ...idProps}
430+
},
431+
[...componentPath],
432+
state.paths,
433+
state.paths.events
434+
)
435+
)
436+
);
413437
});
414438
};
415439
}

dash/dash-renderer/src/utils/clientsideFunctions.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import {updateProps, notifyObservers} from '../actions/index';
2-
import {getPath} from '../actions/paths';
1+
import {updateProps, notifyObservers, setPaths} from '../actions/index';
2+
import {computePaths, getPath} from '../actions/paths';
3+
import {getComponentLayout} from '../wrapper/wrapping';
34
import {getStores} from './stores';
45

56
/**
@@ -16,9 +17,9 @@ function set_props(
1617
for (let y = 0; y < ds.length; y++) {
1718
const {dispatch, getState} = ds[y];
1819
let componentPath;
19-
const {paths} = getState();
20+
const state = getState();
2021
if (!Array.isArray(idOrPath)) {
21-
componentPath = getPath(paths, idOrPath);
22+
componentPath = getPath(state.paths, idOrPath);
2223
} else {
2324
componentPath = idOrPath;
2425
}
@@ -30,6 +31,21 @@ function set_props(
3031
})
3132
);
3233
dispatch(notifyObservers({id: idOrPath, props}));
34+
const oldComponent = getComponentLayout(componentPath, state);
35+
36+
dispatch(
37+
setPaths(
38+
computePaths(
39+
{
40+
...oldComponent,
41+
props: {...oldComponent.props, ...props}
42+
},
43+
[...componentPath],
44+
state.paths,
45+
state.paths.events
46+
)
47+
)
48+
);
3349
}
3450
}
3551

tests/integration/callbacks/test_arbitrary_callbacks.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,44 @@ def test_arb007_clientside_no_output(dash_duo):
232232
dash_duo.wait_for_text_to_equal("#output", "start1")
233233
dash_duo.find_element("#start2").click()
234234
dash_duo.wait_for_text_to_equal("#output", "start2")
235+
236+
237+
def test_arb008_set_props_chain_cb(dash_duo):
238+
app = Dash(suppress_callback_exceptions=True)
239+
240+
app.layout = html.Div(
241+
[
242+
html.Button("origin button", id="origin-button"),
243+
html.Div(id="generated-button-container"),
244+
html.Div("initial text", id="generated-button-output"),
245+
],
246+
style={"padding": 50},
247+
)
248+
249+
@app.callback(
250+
Input("origin-button", "n_clicks"),
251+
)
252+
def generate_button(n_clicks):
253+
set_props(
254+
"generated-button-container",
255+
{
256+
"children": html.Button(
257+
"generated button", id="generated-button", n_clicks=0
258+
)
259+
},
260+
)
261+
262+
@app.callback(
263+
Output("generated-button-output", "children"),
264+
Input("generated-button", "n_clicks", allow_optional=True),
265+
prevent_initial_call=True,
266+
)
267+
def update_output(n_clicks):
268+
return f"n_clicks: {n_clicks}"
269+
270+
dash_duo.start_server(app)
271+
272+
dash_duo.wait_for_element("#origin-button").click()
273+
for i in range(1, 5):
274+
dash_duo.wait_for_element("#generated-button").click()
275+
dash_duo.wait_for_text_to_equal("#generated-button-output", f"n_clicks: {i}")

0 commit comments

Comments
 (0)