diff --git a/@plotly/dash-test-components/src/components/RenderType.js b/@plotly/dash-test-components/src/components/RenderType.js
new file mode 100644
index 0000000000..aa99f1feb8
--- /dev/null
+++ b/@plotly/dash-test-components/src/components/RenderType.js
@@ -0,0 +1,23 @@
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+
+const RenderType = (props) => {
+ const onClick = () => {
+ props.setProps({n_clicks: (props.n_clicks || 0) + 1})
+ }
+
+ return
+ {props.dashRenderType}
+
+
;
+};
+
+RenderType.propTypes = {
+ id: PropTypes.string,
+ dashRenderType: PropTypes.string,
+ n_clicks: PropTypes.number,
+ setProps: PropTypes.func
+};
+
+RenderType.dashRenderType = true;
+export default RenderType;
diff --git a/@plotly/dash-test-components/src/index.js b/@plotly/dash-test-components/src/index.js
index f72bfd0521..9a6523b22c 100644
--- a/@plotly/dash-test-components/src/index.js
+++ b/@plotly/dash-test-components/src/index.js
@@ -7,6 +7,7 @@ import MyPersistedComponentNested from './components/MyPersistedComponentNested'
import StyledComponent from './components/StyledComponent';
import WidthComponent from './components/WidthComponent';
import ComponentAsProp from './components/ComponentAsProp';
+import RenderType from './components/RenderType';
import DrawCounter from './components/DrawCounter';
import AddPropsComponent from "./components/AddPropsComponent";
@@ -32,4 +33,5 @@ export {
ShapeOrExactKeepOrderComponent,
ArrayOfExactOrShapeWithNodePropAssignNone,
ExternalComponent,
+ RenderType
};
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6247e0a0d3..6681cbaef6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,10 +6,17 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## Changed
- [#3113](https://github.com/plotly/dash/pull/3113) Adjusted background polling requests to strip the data from the request, this allows for context to flow as normal. This addresses issue [#3111](https://github.com/plotly/dash/pull/3111)
+- [#3248](https://github.com/plotly/dash/pull/3248) Changes to rendering logic:
+ - if it is first time rendering, render from the parent props
+ - listens only to updates for that single component, no children listening to parents
+ - if parents change a prop with components as props, only the prop changed re-renders, this is then forced on all children regardless of whether or not the props changed
## Fixed
- [#3251](https://github.com/plotly/dash/pull/3251). Prevented default styles from overriding `className_*` props in `dcc.Upload` component.
+## Added
+- [#3248](https://github.com/plotly/dash/pull/3248) added new `dashRenderType` to determine why the component layout was changed (`internal`, `callback`, `parent`, `clientsideApi`):
+ - this can be utilized to keep from rendering components by the component having `dashRenderType` defined as a prop, and the `dashRenderType = true` must be set on the component, eg (`Div.dashRenderType = true`)
## [3.0.1] - 2025-03-24
diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts
index a61dfd6159..9765c8bba1 100644
--- a/dash/dash-renderer/src/actions/callbacks.ts
+++ b/dash/dash-renderer/src/actions/callbacks.ts
@@ -360,7 +360,8 @@ function updateComponent(component_id: any, props: any, cb: ICallbackPayload) {
dispatch(
updateProps({
props,
- itempath: componentPath
+ itempath: componentPath,
+ renderType: 'callback'
})
);
dispatch(notifyObservers({id: component_id, props}));
diff --git a/dash/dash-renderer/src/observers/executedCallbacks.ts b/dash/dash-renderer/src/observers/executedCallbacks.ts
index 1249252704..f82a24bdf0 100644
--- a/dash/dash-renderer/src/observers/executedCallbacks.ts
+++ b/dash/dash-renderer/src/observers/executedCallbacks.ts
@@ -63,12 +63,12 @@ const observer: IStoreObserverDefinition = {
// In case the update contains whole components, see if any of
// those components have props to update to persist user edits.
const {props} = applyPersistence({props: updatedProps}, dispatch);
-
dispatch(
updateProps({
itempath,
props,
- source: 'response'
+ source: 'response',
+ renderType: 'callback'
})
);
diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js
index 908cd87bc6..56d2c82ad2 100644
--- a/dash/dash-renderer/src/reducers/reducer.js
+++ b/dash/dash-renderer/src/reducers/reducer.js
@@ -27,7 +27,7 @@ export const apiRequests = [
'loginRequest'
];
-function layoutHashes(state = {}, action) {
+const layoutHashes = (state = {}, action) => {
if (
includes(action.type, [
'UNDO_PROP_CHANGE',
@@ -37,12 +37,21 @@ function layoutHashes(state = {}, action) {
) {
// Let us compare the paths sums to get updates without triggering
// render on the parent containers.
- const strPath = stringifyPath(action.payload.itempath);
- const prev = pathOr(0, [strPath], state);
- return assoc(strPath, prev + 1, state);
+ const actionPath = action.payload.itempath;
+ const strPath = stringifyPath(actionPath);
+ const prev = pathOr(0, [strPath, 'hash'], state);
+ state = assoc(
+ strPath,
+ {
+ hash: prev + 1,
+ changedProps: action.payload.props,
+ renderType: action.payload.renderType
+ },
+ state
+ );
}
return state;
-}
+};
function mainReducer() {
const parts = {
diff --git a/dash/dash-renderer/src/utils/clientsideFunctions.ts b/dash/dash-renderer/src/utils/clientsideFunctions.ts
index f266c72790..4859df41bf 100644
--- a/dash/dash-renderer/src/utils/clientsideFunctions.ts
+++ b/dash/dash-renderer/src/utils/clientsideFunctions.ts
@@ -16,8 +16,8 @@ function set_props(
for (let y = 0; y < ds.length; y++) {
const {dispatch, getState} = ds[y];
let componentPath;
+ const {paths} = getState();
if (!Array.isArray(idOrPath)) {
- const {paths} = getState();
componentPath = getPath(paths, idOrPath);
} else {
componentPath = idOrPath;
@@ -25,7 +25,8 @@ function set_props(
dispatch(
updateProps({
props,
- itempath: componentPath
+ itempath: componentPath,
+ renderType: 'clientsideApi'
})
);
dispatch(notifyObservers({id: idOrPath, props}));
diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx
index 86c34cc7c6..82bee5b0ba 100644
--- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx
+++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx
@@ -1,4 +1,4 @@
-import React, {useMemo, useCallback, memo} from 'react';
+import React, {useCallback, MutableRefObject, useRef, useMemo} from 'react';
import {
path,
concat,
@@ -24,7 +24,12 @@ import {DashConfig} from '../config';
import {notifyObservers, onError, updateProps} from '../actions';
import {getWatchedKeys, stringifyId} from '../actions/dependencies';
import {recordUiEdit} from '../persistence';
-import {createElement, getComponentLayout, isDryComponent} from './wrapping';
+import {
+ createElement,
+ getComponentLayout,
+ isDryComponent,
+ checkRenderTypeProp
+} from './wrapping';
import Registry from '../registry';
import isSimpleComponent from '../isSimpleComponent';
import {
@@ -41,26 +46,54 @@ type DashWrapperProps = {
*/
componentPath: DashLayoutPath;
_dashprivate_error?: any;
+ _passedComponent?: any;
+ _newRender?: any;
+};
+
+// Define a type for the memoized keys
+type MemoizedKeysType = {
+ [key: string]: React.ReactNode | null; // This includes React elements, strings, numbers, etc.
};
function DashWrapper({
componentPath,
_dashprivate_error,
+ _passedComponent, // pass component to the DashWrapper in the event that it is a newRender and there are no layouthashes
+ _newRender, // this is to force the component to newly render regardless of props (redraw and component as props) is passed from the parent
...extras
}: DashWrapperProps) {
const dispatch = useDispatch();
+ const memoizedKeys: MutableRefObject = useRef({});
+ const newRender = useRef(false);
+ let renderComponent: any = null;
+ let renderComponentProps: any = null;
+ let renderH: any = null;
// Get the config for the component as props
const config: DashConfig = useSelector(selectConfig);
- // Select both the component and it's props.
- const [component, componentProps] = useSelector(
- selectDashProps(componentPath),
- selectDashPropsEqualityFn
- );
+ // Select component and it's props, along with render hash, changed props and the reason for render
+ const [component, componentProps, h, changedProps, renderType] =
+ useSelector(selectDashProps(componentPath), selectDashPropsEqualityFn);
+
+ renderComponent = component;
+ renderComponentProps = componentProps;
+ renderH = h;
+
+ useMemo(() => {
+ if (_newRender) {
+ newRender.current = true;
+ renderH = 0;
+ if (renderH in memoizedKeys.current) {
+ delete memoizedKeys.current[renderH];
+ }
+ } else {
+ newRender.current = false;
+ }
+ }, [_newRender]);
const setProps = (newProps: UpdatePropsPayload) => {
- const {id} = componentProps;
+ const {id} = renderComponentProps;
const {_dash_error, ...restProps} = newProps;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -68,11 +101,10 @@ function DashWrapper({
dispatch((dispatch, getState) => {
const currentState = getState();
const {graphs} = currentState;
-
- const {props: oldProps} = getComponentLayout(
- componentPath,
- currentState
- );
+ const oldLayout = getComponentLayout(componentPath, currentState);
+ if (!oldLayout) return;
+ const {props: oldProps} = oldLayout;
+ if (!oldProps) return;
const changedProps = pickBy(
(val, key) => !equals(val, oldProps[key]),
restProps
@@ -96,7 +128,7 @@ function DashWrapper({
batch(() => {
// setProps here is triggered by the UI - record these changes
// for persistence
- recordUiEdit(component, newProps, dispatch);
+ recordUiEdit(renderComponent, newProps, dispatch);
// Only dispatch changes to Dash if a watched prop changed
if (watchedKeys.length) {
@@ -112,7 +144,8 @@ function DashWrapper({
dispatch(
updateProps({
props: changedProps,
- itempath: componentPath
+ itempath: componentPath,
+ renderType: 'internal'
})
);
});
@@ -120,9 +153,9 @@ function DashWrapper({
};
const createContainer = useCallback(
- (container, containerPath, key = undefined) => {
- if (isSimpleComponent(component)) {
- return component;
+ (container, containerPath, _childNewRender, key = undefined) => {
+ if (isSimpleComponent(renderComponent)) {
+ return renderComponent;
}
return (
);
},
@@ -141,7 +176,7 @@ function DashWrapper({
);
const wrapChildrenProp = useCallback(
- (node: any, childrenProp: DashLayoutPath) => {
+ (node: any, childrenPath: DashLayoutPath, _childNewRender: any) => {
if (Array.isArray(node)) {
return node.map((n, i) => {
if (isDryComponent(n)) {
@@ -149,9 +184,10 @@ function DashWrapper({
n,
concat(componentPath, [
'props',
- ...childrenProp,
+ ...childrenPath,
i
]),
+ _childNewRender,
i
);
}
@@ -163,7 +199,8 @@ function DashWrapper({
}
return createContainer(
node,
- concat(componentPath, ['props', ...childrenProp])
+ concat(componentPath, ['props', ...childrenPath]),
+ _childNewRender
);
},
[componentPath]
@@ -172,25 +209,42 @@ function DashWrapper({
const extraProps = {
setProps,
...extras
- };
+ } as {[key: string]: any};
- const element = useMemo(() => Registry.resolve(component), [component]);
+ if (checkRenderTypeProp(renderComponent)) {
+ extraProps['dashRenderType'] = newRender.current
+ ? 'parent'
+ : changedProps
+ ? renderType
+ : 'parent';
+ }
- const hydratedProps = useMemo(() => {
+ const setHydratedProps = (component: any, componentProps: any) => {
// Hydrate components props
const childrenProps = pathOr(
[],
- ['children_props', component.namespace, component.type],
+ ['children_props', component?.namespace, component?.type],
config
);
let props = mergeRight(dissoc('children', componentProps), extraProps);
for (let i = 0; i < childrenProps.length; i++) {
const childrenProp: string = childrenProps[i];
-
+ let childNewRender: any = 0;
+ if (
+ childrenProp
+ .split('.')[0]
+ .replace('[]', '')
+ .replace('{}', '') in changedProps ||
+ newRender.current ||
+ !renderH
+ ) {
+ childNewRender = {};
+ }
const handleObject = (obj: any, opath: DashLayoutPath) => {
return mapObjIndexed(
- (node, k) => wrapChildrenProp(node, [...opath, k]),
+ (node, k) =>
+ wrapChildrenProp(node, [...opath, k], childNewRender),
obj
);
};
@@ -260,7 +314,8 @@ function DashWrapper({
} else {
listValue = wrapChildrenProp(
path(backPath, el),
- elementPath
+ elementPath,
+ childNewRender
);
}
return assocPath(backPath, listValue, el);
@@ -306,7 +361,8 @@ function DashWrapper({
dynamic,
concat([k], backPath)
)
- : concat(dynamic, [k])
+ : concat(dynamic, [k]),
+ childNewRender
),
dynValue
);
@@ -317,7 +373,11 @@ function DashWrapper({
if (node === undefined) {
continue;
}
- nodeValue = wrapChildrenProp(node, childrenPath);
+ nodeValue = wrapChildrenProp(
+ node,
+ childrenPath,
+ childNewRender
+ );
}
}
props = assocPath(childrenPath, nodeValue, props);
@@ -353,7 +413,11 @@ function DashWrapper({
if (node !== undefined) {
props = assoc(
childrenProp,
- wrapChildrenProp(node, [childrenProp]),
+ wrapChildrenProp(
+ node,
+ [childrenProp],
+ childNewRender
+ ),
props
);
}
@@ -367,84 +431,82 @@ function DashWrapper({
props.id = stringifyId(props.id);
}
return props;
- }, [componentProps]);
+ };
- const hydrated = useMemo(() => {
- let hydratedChildren: any;
- if (componentProps.children !== undefined) {
- hydratedChildren = wrapChildrenProp(componentProps.children, [
- 'children'
- ]);
+ const hydrateFunc = () => {
+ if (newRender.current) {
+ renderComponent = _passedComponent;
+ renderComponentProps = _passedComponent?.props;
}
- if (config.props_check) {
- return (
-
- {createElement(
- element,
- hydratedProps,
- extraProps,
- hydratedChildren
- )}
-
+ if (!renderComponent) {
+ return null;
+ }
+
+ const element = Registry.resolve(renderComponent);
+ const hydratedProps = setHydratedProps(
+ renderComponent,
+ renderComponentProps
+ );
+
+ let hydratedChildren: any;
+ if (renderComponentProps.children !== undefined) {
+ hydratedChildren = wrapChildrenProp(
+ renderComponentProps.children,
+ ['children'],
+ !renderH || newRender.current || 'children' in changedProps
+ ? {}
+ : 0
);
}
+ newRender.current = false;
- return createElement(
- element,
- hydratedProps,
- extraProps,
- hydratedChildren
+ return config.props_check ? (
+
+ {createElement(
+ element,
+ hydratedProps,
+ extraProps,
+ hydratedChildren
+ )}
+
+ ) : (
+ createElement(element, hydratedProps, extraProps, hydratedChildren)
);
- }, [
- element,
- component,
- hydratedProps,
- extraProps,
- wrapChildrenProp,
- componentProps,
- config.props_check
- ]);
-
- return (
+ };
+
+ let hydrated = null;
+ if (renderH in memoizedKeys.current && !newRender.current) {
+ hydrated = React.isValidElement(memoizedKeys.current[renderH])
+ ? memoizedKeys.current[renderH]
+ : null;
+ }
+ if (!hydrated) {
+ hydrated = hydrateFunc();
+ memoizedKeys.current = {[renderH]: hydrated};
+ }
+
+ return renderComponent ? (
- {hydrated}
+ {React.isValidElement(hydrated) ? hydrated : }
+ ) : (
+
);
}
-function wrapperEquality(prev: any, next: any) {
- const {
- componentPath: prevPath,
- _dashprivate_error: prevError,
- ...prevProps
- } = prev;
- const {
- componentPath: nextPath,
- _dashprivate_error: nextError,
- ...nextProps
- } = next;
- if (JSON.stringify(prevPath) !== JSON.stringify(nextPath)) {
- return false;
- }
- if (prevError !== nextError) {
- return false;
- }
- return equals(prevProps, nextProps);
-}
-
-export default memo(DashWrapper, wrapperEquality);
+export default DashWrapper;
diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts
index 293af71264..46930bc47e 100644
--- a/dash/dash-renderer/src/wrapper/selectors.ts
+++ b/dash/dash-renderer/src/wrapper/selectors.ts
@@ -1,7 +1,7 @@
import {DashLayoutPath, DashComponent, BaseDashProps} from '../types/component';
import {getComponentLayout, stringifyPath} from './wrapping';
-type SelectDashProps = [DashComponent, BaseDashProps, number];
+type SelectDashProps = [DashComponent, BaseDashProps, number, object, string];
export const selectDashProps =
(componentPath: DashLayoutPath) =>
@@ -12,14 +12,16 @@ export const selectDashProps =
// Then it can be easily compared without having to compare the props.
const strPath = stringifyPath(componentPath);
- const h = Object.entries(state.layoutHashes).reduce(
- (acc, [updatedPath, pathHash]) =>
- strPath.startsWith(updatedPath)
- ? (pathHash as number) + acc
- : acc,
- 0
- );
- return [c, c.props, h];
+ const hash = state.layoutHashes[strPath];
+ let h = 0;
+ let changedProps: object = {};
+ let renderType = '';
+ if (hash) {
+ h = hash['hash'];
+ changedProps = hash['changedProps'];
+ renderType = hash['renderType'];
+ }
+ return [c, c?.props, h, changedProps, renderType];
};
export function selectDashPropsEqualityFn(
diff --git a/dash/dash-renderer/src/wrapper/wrapping.ts b/dash/dash-renderer/src/wrapper/wrapping.ts
index baf0e4f6f3..37d2c461ea 100644
--- a/dash/dash-renderer/src/wrapper/wrapping.ts
+++ b/dash/dash-renderer/src/wrapper/wrapping.ts
@@ -1,5 +1,5 @@
import React from 'react';
-import {mergeRight, path, type, has, join} from 'ramda';
+import {mergeRight, path, type, has, join, pathOr} from 'ramda';
import {DashComponent, DashLayoutPath} from '../types/component';
export function createElement(
@@ -61,3 +61,14 @@ export function getComponentLayout(
): DashComponent {
return path(componentPath, state.layout) as DashComponent;
}
+
+export function checkRenderTypeProp(componentDefinition: any) {
+ return (
+ 'dashRenderType' in
+ pathOr(
+ {},
+ [componentDefinition?.namespace, componentDefinition?.type],
+ window as any
+ )
+ );
+}
diff --git a/tests/integration/renderer/test_component_as_prop.py b/tests/integration/renderer/test_component_as_prop.py
index 9bfd5d0b6f..96fa938f2f 100644
--- a/tests/integration/renderer/test_component_as_prop.py
+++ b/tests/integration/renderer/test_component_as_prop.py
@@ -25,138 +25,139 @@ def opt(u):
def test_rdcap001_component_as_prop(dash_duo):
app = Dash(__name__)
- app.layout = Div(
- [
- ComponentAsProp(
- element=Div(
- "as-props",
- id="as-props",
- )
- ),
- ComponentAsProp(
- id="clicker-container", element=Button("click-me", id="clicker")
- ),
- ComponentAsProp(
- id="nested-output-container",
- element=Div(id="nested-output"),
- ),
- Div(
- [
- Button("click-nested", id="send-nested"),
- Div(id="output-from-prop"),
- ]
- ),
- ComponentAsProp(
- id="elements",
- element=[
- Div("one", id="list-one"),
- Div("two", id="list-two"),
- Div(id="list-output"),
- ],
- ),
- Div(
- [
- Button("click-list", id="to-list"),
- Div(id="output-from-list"),
- Button("click footer", id="to-footer"),
- Div(id="from-header"),
- Div(id="from-list-of-dict"),
- Button("click to list", id="update-list-of-dict"),
- ],
- ),
- ComponentAsProp(
- id="shaped",
- shapeEl={
- "header": Button("header", id="button-header"),
- "footer": Div("initial", id="footer"),
+ content = [
+ ComponentAsProp(
+ element=Div(
+ "as-props",
+ id="as-props",
+ )
+ ),
+ ComponentAsProp(
+ id="clicker-container", element=Button("click-me", id="clicker")
+ ),
+ ComponentAsProp(
+ id="nested-output-container",
+ element=Div(id="nested-output"),
+ ),
+ Div(
+ [
+ Button("click-nested", id="send-nested"),
+ Div(id="output-from-prop"),
+ ]
+ ),
+ ComponentAsProp(
+ id="elements",
+ element=[
+ Div("one", id="list-one"),
+ Div("two", id="list-two"),
+ Div(id="list-output"),
+ ],
+ ),
+ Div(
+ [
+ Button("click-list", id="to-list"),
+ Div(id="output-from-list"),
+ Button("click footer", id="to-footer"),
+ Div(id="from-header"),
+ Div(id="from-list-of-dict"),
+ Button("click to list", id="update-list-of-dict"),
+ ],
+ ),
+ ComponentAsProp(
+ id="shaped",
+ shapeEl={
+ "header": Button("header", id="button-header"),
+ "footer": Div("initial", id="footer"),
+ },
+ ),
+ ComponentAsProp(
+ id="list-of-dict",
+ list_of_shapes=[
+ {"label": Button(f"click-{i}", id=f"list-click-{i}"), "value": i}
+ for i in range(1, 4)
+ ],
+ ),
+ ComponentAsProp(
+ "list-of-dict-update",
+ list_of_shapes=[
+ {
+ "label": Div("update me", id="update-in-list-of-dict"),
+ "value": 1,
},
- ),
- ComponentAsProp(
- id="list-of-dict",
- list_of_shapes=[
- {"label": Button(f"click-{i}", id=f"list-click-{i}"), "value": i}
- for i in range(1, 4)
- ],
- ),
- ComponentAsProp(
- "list-of-dict-update",
- list_of_shapes=[
- {
- "label": Div("update me", id="update-in-list-of-dict"),
- "value": 1,
- },
- ],
- ),
- ComponentAsProp(
- id="list-of-list-of-nodes",
- list_of_shapes=[
- {
- "label": [
- Div("first-label", id="first-label"),
- Div("second-label", id="second-label"),
- ],
- "value": 2,
- }
- ],
- ),
- ComponentAsProp(
- id="list-in-shape",
- shapeEl={
- "header": [
- Div("one", id="first-in-shape"),
- Div("two", id="second-in-shape"),
- ]
+ ],
+ ),
+ ComponentAsProp(
+ id="list-of-list-of-nodes",
+ list_of_shapes=[
+ {
+ "label": [
+ Div("first-label", id="first-label"),
+ Div("second-label", id="second-label"),
+ ],
+ "value": 2,
+ }
+ ],
+ ),
+ ComponentAsProp(
+ id="list-in-shape",
+ shapeEl={
+ "header": [
+ Div("one", id="first-in-shape"),
+ Div("two", id="second-in-shape"),
+ ]
+ },
+ ),
+ ComponentAsProp(
+ id="multi-component",
+ multi_components=[
+ {
+ "id": "multi",
+ "first": Span("first"),
+ "second": Span("second"),
},
- ),
- ComponentAsProp(
- id="multi-component",
- multi_components=[
- {
- "id": "multi",
- "first": Span("first"),
- "second": Span("second"),
- },
- {
- "id": "multi2",
- "first": Span("foo"),
- "second": Span("bar"),
- },
- ],
- ),
- ComponentAsProp(
- dynamic={
- "inside-dynamic": Div("dynamic", "inside-dynamic"),
- "output-dynamic": Div(id="output-dynamic"),
- "clicker": Button("click-dynamic", id="click-dynamic"),
- "clicker-dict": Button("click-dict", id="click-dict"),
- "clicker-list": Button("click-list", id="click-list"),
- "clicker-nested": Button("click-nested", id="click-nested"),
+ {
+ "id": "multi2",
+ "first": Span("foo"),
+ "second": Span("bar"),
},
- dynamic_dict={
- "node": {
- "dict-dyn": Div("dict-dyn", id="inside-dict"),
- "dict-2": Div("dict-2", id="inside-dict-2"),
- }
+ ],
+ ),
+ ComponentAsProp(
+ dynamic={
+ "inside-dynamic": Div("dynamic", "inside-dynamic"),
+ "output-dynamic": Div(id="output-dynamic"),
+ "clicker": Button("click-dynamic", id="click-dynamic"),
+ "clicker-dict": Button("click-dict", id="click-dict"),
+ "clicker-list": Button("click-list", id="click-list"),
+ "clicker-nested": Button("click-nested", id="click-nested"),
+ },
+ dynamic_dict={
+ "node": {
+ "dict-dyn": Div("dict-dyn", id="inside-dict"),
+ "dict-2": Div("dict-2", id="inside-dict-2"),
+ }
+ },
+ dynamic_list=[
+ {
+ "list": Div("dynamic-list", id="inside-list"),
+ "list-2": Div("list-2", id="inside-list-2"),
},
- dynamic_list=[
- {
- "list": Div("dynamic-list", id="inside-list"),
- "list-2": Div("list-2", id="inside-list-2"),
+ {"list-3": Div("list-3", id="inside-list-3")},
+ ],
+ dynamic_nested_list=[
+ {"obj": {"nested": Div("nested", id="nested-dyn")}},
+ {
+ "obj": {
+ "nested": Div("nested-2", id="nested-2"),
+ "nested-again": Div("nested-again", id="nested-again"),
},
- {"list-3": Div("list-3", id="inside-list-3")},
- ],
- dynamic_nested_list=[
- {"obj": {"nested": Div("nested", id="nested-dyn")}},
- {
- "obj": {
- "nested": Div("nested-2", id="nested-2"),
- "nested-again": Div("nested-again", id="nested-again"),
- },
- },
- ],
- ),
- ]
- )
+ },
+ ],
+ ),
+ Button(id="reset_test"),
+ ]
+
+ app.layout = Div(content, id="test_container")
@app.callback(
Output("output-from-prop", "children"), [Input("clicker", "n_clicks")]
@@ -237,80 +238,94 @@ def on_click(n_clicks):
def on_click(n_clicks):
return f"Clicked {n_clicks}"
+ @app.callback(
+ Output("test_container", "children"),
+ Input("reset_test", "n_clicks"),
+ prevent_initial_call=True,
+ )
+ def on_click(_):
+ return content
+
dash_duo.start_server(app)
- assert dash_duo.get_logs() == []
+ for i in range(3):
+ ## tests re-rendering of components as props outside of the normal layout or first-time render
+ assert dash_duo.get_logs() == []
- dash_duo.wait_for_text_to_equal("#as-props", "as-props")
+ dash_duo.wait_for_text_to_equal("#as-props", "as-props")
- elements = dash_duo.find_elements("#elements div")
+ elements = dash_duo.find_elements("#elements div")
- assert len(elements) == 3
+ assert len(elements) == 3
- clicker = dash_duo.wait_for_element("#clicker")
- clicker.click()
- dash_duo.wait_for_text_to_equal("#output-from-prop", "From prop: 1")
+ clicker = dash_duo.wait_for_element("#clicker")
+ clicker.click()
+ dash_duo.wait_for_text_to_equal("#output-from-prop", "From prop: 1")
- nested = dash_duo.wait_for_element("#send-nested")
- nested.click()
- dash_duo.wait_for_text_to_equal("#nested-output", "Nested: 1")
+ nested = dash_duo.wait_for_element("#send-nested")
+ nested.click()
+ dash_duo.wait_for_text_to_equal("#nested-output", "Nested: 1")
- to_list = dash_duo.find_element("#to-list")
- to_list.click()
- dash_duo.wait_for_text_to_equal("#list-output", "To list: 1")
+ to_list = dash_duo.find_element("#to-list")
+ to_list.click()
+ dash_duo.wait_for_text_to_equal("#list-output", "To list: 1")
- from_list = dash_duo.find_element("#list-two")
- from_list.click()
- dash_duo.wait_for_text_to_equal("#output-from-list", "From list: 1")
+ from_list = dash_duo.find_element("#list-two")
+ from_list.click()
+ dash_duo.wait_for_text_to_equal("#output-from-list", "From list: 1")
- from_header = dash_duo.find_element("#button-header")
- from_header.click()
- dash_duo.wait_for_text_to_equal("#from-header", "From header: 1")
+ from_header = dash_duo.find_element("#button-header")
+ from_header.click()
+ dash_duo.wait_for_text_to_equal("#from-header", "From header: 1")
- to_footer = dash_duo.find_element("#to-footer")
- to_footer.click()
- dash_duo.wait_for_text_to_equal("#footer", "To footer: 1")
+ to_footer = dash_duo.find_element("#to-footer")
+ to_footer.click()
+ dash_duo.wait_for_text_to_equal("#footer", "To footer: 1")
- for btn_id in (f"list-click-{i}" for i in range(1, 4)):
- dash_duo.find_element(f"#{btn_id}").click()
- dash_duo.wait_for_text_to_equal("#from-list-of-dict", f"{btn_id}.n_clicks")
+ for btn_id in (f"list-click-{i}" for i in range(1, 4)):
+ dash_duo.find_element(f"#{btn_id}").click()
+ dash_duo.wait_for_text_to_equal("#from-list-of-dict", f"{btn_id}.n_clicks")
- dash_duo.find_element("#update-list-of-dict").click()
- dash_duo.wait_for_text_to_equal("#update-in-list-of-dict", "Updated: 1")
+ dash_duo.find_element("#update-list-of-dict").click()
+ dash_duo.wait_for_text_to_equal("#update-in-list-of-dict", "Updated: 1")
- dash_duo.wait_for_text_to_equal("#first-label", "first-label")
- dash_duo.wait_for_text_to_equal("#second-label", "second-label")
+ dash_duo.wait_for_text_to_equal("#first-label", "first-label")
+ dash_duo.wait_for_text_to_equal("#second-label", "second-label")
- dash_duo.wait_for_text_to_equal("#first-in-shape", "one")
- dash_duo.wait_for_text_to_equal("#second-in-shape", "two")
+ dash_duo.wait_for_text_to_equal("#first-in-shape", "one")
+ dash_duo.wait_for_text_to_equal("#second-in-shape", "two")
- dash_duo.wait_for_text_to_equal("#multi", "first - second")
- dash_duo.wait_for_text_to_equal("#multi2", "foo - bar")
+ dash_duo.wait_for_text_to_equal("#multi", "first - second")
+ dash_duo.wait_for_text_to_equal("#multi2", "foo - bar")
- dash_duo.wait_for_text_to_equal("#inside-dynamic", "dynamic")
- dash_duo.wait_for_text_to_equal("#dict-dyn", "dict-dyn")
- dash_duo.wait_for_text_to_equal("#inside-dict-2", "dict-2")
- dash_duo.wait_for_text_to_equal("#nested-2", "nested-2")
- dash_duo.wait_for_text_to_equal("#nested-again", "nested-again")
+ dash_duo.wait_for_text_to_equal("#inside-dynamic", "dynamic")
+ dash_duo.wait_for_text_to_equal("#dict-dyn", "dict-dyn")
+ dash_duo.wait_for_text_to_equal("#inside-dict-2", "dict-2")
+ dash_duo.wait_for_text_to_equal("#nested-2", "nested-2")
+ dash_duo.wait_for_text_to_equal("#nested-again", "nested-again")
- dash_duo.wait_for_text_to_equal("#inside-list", "dynamic-list")
- dash_duo.wait_for_text_to_equal("#inside-list-2", "list-2")
- dash_duo.wait_for_text_to_equal("#inside-list-3", "list-3")
+ dash_duo.wait_for_text_to_equal("#inside-list", "dynamic-list")
+ dash_duo.wait_for_text_to_equal("#inside-list-2", "list-2")
+ dash_duo.wait_for_text_to_equal("#inside-list-3", "list-3")
- dash_duo.find_element("#click-dynamic").click()
- dash_duo.wait_for_text_to_equal("#output-dynamic", "Clicked 1")
+ dash_duo.find_element("#click-dynamic").click()
+ dash_duo.wait_for_text_to_equal("#output-dynamic", "Clicked 1")
- dash_duo.find_element("#click-dict").click()
- dash_duo.wait_for_text_to_equal("#inside-dict", "Clicked 1")
+ dash_duo.find_element("#click-dict").click()
+ dash_duo.wait_for_text_to_equal("#inside-dict", "Clicked 1")
- dash_duo.find_element("#click-list").click()
- dash_duo.wait_for_text_to_equal("#inside-list", "Clicked 1")
+ dash_duo.find_element("#click-list").click()
+ dash_duo.wait_for_text_to_equal("#inside-list", "Clicked 1")
- dash_duo.find_element("#click-nested").click()
- dash_duo.wait_for_text_to_equal("#nested-dyn", "Clicked 1")
+ dash_duo.find_element("#click-nested").click()
+ dash_duo.wait_for_text_to_equal("#nested-dyn", "Clicked 1")
+ assert dash_duo.get_logs() == []
+ dash_duo.find_element("#reset_test").click()
assert dash_duo.get_logs() == []
+ dash_duo.wait_for_text_to_equal("#as-props", "as-props")
+
def test_rdcap002_component_as_props_dynamic_id(dash_duo):
# Test for issue 2296
diff --git a/tests/integration/renderer/test_render_type.py b/tests/integration/renderer/test_render_type.py
new file mode 100644
index 0000000000..17a6cfbae3
--- /dev/null
+++ b/tests/integration/renderer/test_render_type.py
@@ -0,0 +1,66 @@
+from dash import Dash, Input, Output, html
+import json
+
+import dash_test_components as dt
+
+
+def test_rtype001_rendertype(dash_duo):
+ app = Dash()
+
+ app.layout = html.Div(
+ [
+ html.Div(
+ dt.RenderType(id="render_test"),
+ id="container",
+ ),
+ html.Button("redraw", id="redraw"),
+ html.Button("update render", id="update_render"),
+ html.Button("clientside", id="clientside_render"),
+ html.Div(id="render_output"),
+ ]
+ )
+
+ app.clientside_callback(
+ """(n) => {
+ dash_clientside.set_props('render_test', {n_clicks: 20})
+ }""",
+ Input("clientside_render", "n_clicks"),
+ )
+
+ @app.callback(
+ Output("container", "children"),
+ Input("redraw", "n_clicks"),
+ prevent_initial_call=True,
+ )
+ def on_click(_):
+ return dt.RenderType(id="render_test")
+
+ @app.callback(
+ Output("render_test", "n_clicks"),
+ Input("update_render", "n_clicks"),
+ prevent_initial_call=True,
+ )
+ def update_render(_):
+ return 0
+
+ @app.callback(Output("render_output", "children"), Input("render_test", "n_clicks"))
+ def display_clicks(n):
+ return json.dumps(n)
+
+ dash_duo.start_server(app)
+
+ render_type = "#render_test > span"
+ render_output = "#render_output"
+ dash_duo.wait_for_text_to_equal(render_type, "parent")
+ dash_duo.find_element("#update_render").click()
+ dash_duo.wait_for_text_to_equal(render_type, "callback")
+ dash_duo.wait_for_text_to_equal(render_output, "0")
+ dash_duo.find_element("#clientside_render").click()
+ dash_duo.wait_for_text_to_equal(render_type, "clientsideApi")
+ dash_duo.wait_for_text_to_equal(render_output, "20")
+ dash_duo.find_element("#render_test > button").click()
+ dash_duo.wait_for_text_to_equal(render_type, "internal")
+ dash_duo.wait_for_text_to_equal(render_output, "21")
+ dash_duo.find_element("#redraw").click()
+ dash_duo.wait_for_text_to_equal(render_type, "parent")
+ dash_duo.wait_for_text_to_equal(render_output, "null")
diff --git a/tests/integration/test_patch.py b/tests/integration/test_patch.py
index 99041a6902..c2b3b3f500 100644
--- a/tests/integration/test_patch.py
+++ b/tests/integration/test_patch.py
@@ -5,6 +5,7 @@
from selenium.webdriver.common.keys import Keys
from dash import Dash, html, dcc, Input, Output, State, ALL, Patch
+from dash.testing.wait import until
@flaky.flaky(max_runs=3)
@@ -219,52 +220,58 @@ def get_output():
_input.send_keys("Set Value")
dash_duo.find_element("#set-btn").click()
- assert get_output()["value"] == "Set Value"
+ until(lambda: get_output()["value"] == "Set Value", 2)
_input = dash_duo.find_element("#append-value")
_input.send_keys("Append")
dash_duo.find_element("#append-btn").click()
- assert get_output()["array"] == ["initial", "Append"]
+ until(lambda: get_output()["array"] == ["initial", "Append"], 2)
_input = dash_duo.find_element("#prepend-value")
_input.send_keys("Prepend")
dash_duo.find_element("#prepend-btn").click()
- assert get_output()["array"] == ["Prepend", "initial", "Append"]
+ until(lambda: get_output()["array"] == ["Prepend", "initial", "Append"], 2)
_input = dash_duo.find_element("#extend-value")
_input.send_keys("Extend")
dash_duo.find_element("#extend-btn").click()
- assert get_output()["array"] == ["Prepend", "initial", "Append", "Extend"]
+ until(
+ lambda: get_output()["array"] == ["Prepend", "initial", "Append", "Extend"], 2
+ )
undef = object()
- assert get_output().get("merge", undef) is undef
+ until(lambda: get_output().get("merge", undef) is undef, 2)
_input = dash_duo.find_element("#merge-value")
_input.send_keys("Merged")
dash_duo.find_element("#merge-btn").click()
- assert get_output()["merged"] == "Merged"
+ until(lambda: get_output()["merged"] == "Merged", 2)
- assert get_output()["delete"] == "Delete me"
+ until(lambda: get_output()["delete"] == "Delete me", 2)
dash_duo.find_element("#delete-btn").click()
- assert get_output().get("delete", undef) is undef
+ until(lambda: get_output().get("delete", undef) is undef, 2)
_input = dash_duo.find_element("#insert-value")
_input.send_keys("Inserted")
dash_duo.find_element("#insert-btn").click()
- assert get_output().get("array") == [
- "Prepend",
- "Inserted",
- "initial",
- "Append",
- "Extend",
- ]
+ until(
+ lambda: get_output().get("array")
+ == [
+ "Prepend",
+ "Inserted",
+ "initial",
+ "Append",
+ "Extend",
+ ],
+ 2,
+ )
_input.send_keys(" with negative index")
_input = dash_duo.find_element("#insert-index")
@@ -272,40 +279,56 @@ def get_output():
_input.send_keys("-1")
dash_duo.find_element("#insert-btn").click()
- assert get_output().get("array") == [
- "Prepend",
- "Inserted",
- "initial",
- "Append",
- "Inserted with negative index",
- "Extend",
- ]
+ until(
+ lambda: get_output().get("array")
+ == [
+ "Prepend",
+ "Inserted",
+ "initial",
+ "Append",
+ "Inserted with negative index",
+ "Extend",
+ ],
+ 2,
+ )
dash_duo.find_element("#delete-index").click()
- assert get_output().get("array") == [
- "Prepend",
- "initial",
- "Append",
- "Extend",
- ]
+ until(
+ lambda: get_output().get("array")
+ == [
+ "Prepend",
+ "initial",
+ "Append",
+ "Extend",
+ ],
+ 2,
+ )
dash_duo.find_element("#reverse-btn").click()
- assert get_output().get("array") == [
- "Extend",
- "Append",
- "initial",
- "Prepend",
- ]
+ until(
+ lambda: get_output().get("array")
+ == [
+ "Extend",
+ "Append",
+ "initial",
+ "Prepend",
+ ],
+ 2,
+ )
dash_duo.find_element("#remove-btn").click()
- assert get_output().get("array") == [
- "Extend",
- "Append",
- "Prepend",
- ]
+ until(
+ lambda: get_output().get("array")
+ == [
+ "Extend",
+ "Append",
+ "Prepend",
+ ],
+ 2,
+ )
dash_duo.find_element("#clear-btn").click()
- assert get_output()["array"] == []
+ until(lambda: get_output()["array"] == [], 2)
def test_pch002_patch_app_pmc_callbacks(dash_duo):