From 393e8709847769a1be2bbf093d7108893b2b6b89 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Wed, 26 Mar 2025 09:25:06 -0400 Subject: [PATCH 01/33] Adjusting memoization on the `DashWrapper` to be utilized --- .../dash-renderer/src/wrapper/DashWrapper.tsx | 30 +++++++++++++------ dash/dash-renderer/src/wrapper/selectors.ts | 8 +---- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 86c34cc7c6..885a2afb9e 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -369,7 +369,27 @@ function DashWrapper({ return props; }, [componentProps]); + const dependenciesStable = useMemo(() => { + return JSON.stringify( + { + element: element, + component: component, + hydratedProps: hydratedProps, + extraProps: extraProps, + wrapChildrenProp: wrapChildrenProp, + componentProps: componentProps + } + ) + }, [element, + component, + hydratedProps, + wrapChildrenProp, + componentProps, + config.props_check + ]) + const hydrated = useMemo(() => { + console.log('rendering') let hydratedChildren: any; if (componentProps.children !== undefined) { hydratedChildren = wrapChildrenProp(componentProps.children, [ @@ -399,15 +419,7 @@ function DashWrapper({ extraProps, hydratedChildren ); - }, [ - element, - component, - hydratedProps, - extraProps, - wrapChildrenProp, - componentProps, - config.props_check - ]); + }, [dependenciesStable]); return ( - strPath.startsWith(updatedPath) - ? (pathHash as number) + acc - : acc, - 0 - ); + const h = state.layoutHashes[strPath]; return [c, c.props, h]; }; From 3c2898db61a8b4ebd061437c0969902ee766329e Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Wed, 26 Mar 2025 10:40:16 -0400 Subject: [PATCH 02/33] adjusting hash for renders --- .../dash-renderer/src/wrapper/DashWrapper.tsx | 18 +++++++----- dash/dash-renderer/src/wrapper/selectors.ts | 29 ++++++++++++++++++- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 885a2afb9e..d8b9600bea 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -54,7 +54,7 @@ function DashWrapper({ const config: DashConfig = useSelector(selectConfig); // Select both the component and it's props. - const [component, componentProps] = useSelector( + const [component, componentProps, h] = useSelector( selectDashProps(componentPath), selectDashPropsEqualityFn ); @@ -372,12 +372,13 @@ function DashWrapper({ const dependenciesStable = useMemo(() => { return JSON.stringify( { - element: element, - component: component, - hydratedProps: hydratedProps, - extraProps: extraProps, - wrapChildrenProp: wrapChildrenProp, - componentProps: componentProps + element, + component, + hydratedProps, + extraProps, + wrapChildrenProp, + componentProps, + h } ) }, [element, @@ -385,7 +386,8 @@ function DashWrapper({ hydratedProps, wrapChildrenProp, componentProps, - config.props_check + config.props_check, + h ]) const hydrated = useMemo(() => { diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index 2eee497420..040b808877 100644 --- a/dash/dash-renderer/src/wrapper/selectors.ts +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -3,6 +3,25 @@ import {getComponentLayout, stringifyPath} from './wrapping'; type SelectDashProps = [DashComponent, BaseDashProps, number]; +const isFirstLevelPropsChild = (updatedPath: string, strPath: string) => { + const updatedSegments = updatedPath.split(','); + const fullSegments = strPath.split(','); + + // Check that strPath actually starts with updatedPath + const startsWithPath = updatedSegments.every( + (seg, i) => fullSegments[i] === seg + ); + + if (!startsWithPath) return false; + + // Get the remaining path after the prefix + const remainingSegments = fullSegments.slice(updatedSegments.length); + + const propsCount = remainingSegments.filter(s => s === 'props').length; + + return propsCount < 2; +} + export const selectDashProps = (componentPath: DashLayoutPath) => (state: any): SelectDashProps => { @@ -12,7 +31,15 @@ export const selectDashProps = // Then it can be easily compared without having to compare the props. const strPath = stringifyPath(componentPath); - const h = state.layoutHashes[strPath]; + const h = Object.entries(state.layoutHashes).reduce( + (acc, [updatedPath, pathHash]) => + { + return isFirstLevelPropsChild(updatedPath, strPath) + ? (pathHash as number) + acc + : acc + }, + 0 + ); return [c, c.props, h]; }; From 25c20def4b53d0d441f7c4fc380acad98c27e163 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Thu, 27 Mar 2025 11:45:47 -0400 Subject: [PATCH 03/33] simplifying and using the reducer to trigger adjustments only if the `children` has been altered --- .../src/observers/executedCallbacks.ts | 6 ++-- dash/dash-renderer/src/reducers/reducer.js | 29 +++++++++++++--- .../src/utils/clientsideFunctions.ts | 6 ++-- .../dash-renderer/src/wrapper/DashWrapper.tsx | 12 +++---- dash/dash-renderer/src/wrapper/selectors.ts | 34 +++---------------- 5 files changed, 42 insertions(+), 45 deletions(-) diff --git a/dash/dash-renderer/src/observers/executedCallbacks.ts b/dash/dash-renderer/src/observers/executedCallbacks.ts index 1249252704..b8be2e517e 100644 --- a/dash/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash/dash-renderer/src/observers/executedCallbacks.ts @@ -45,7 +45,8 @@ const observer: IStoreObserverDefinition = { } = getState(); function applyProps(id: any, updatedProps: any) { - const {layout, paths} = getState(); + const _state = getState() + const {layout, paths} = _state; const itempath = getPath(paths, id); if (!itempath) { return false; @@ -68,7 +69,8 @@ const observer: IStoreObserverDefinition = { updateProps({ itempath, props, - source: 'response' + source: 'response', + state: _state }) ); diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index 908cd87bc6..76128eba1c 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -18,7 +18,7 @@ import layout from './layout'; import paths from './paths'; import callbackJobs from './callbackJobs'; import loading from './loading'; -import {stringifyPath} from '../wrapper/wrapping'; +import {stringifyPath, getComponentLayout} from '../wrapper/wrapping'; export const apiRequests = [ 'dependenciesRequest', @@ -27,6 +27,29 @@ export const apiRequests = [ 'loginRequest' ]; +function adjustHashes(state, action) { + const actionPath = action.payload.itempath + const strPath = stringifyPath(actionPath); + const prev = pathOr(0, [strPath], state); + state = assoc(strPath, prev + 1, state); + + // check if children was adjusted + if ('children' in pathOr({}, ['payload', 'props'], action)) { + const layout = getComponentLayout(action.payload.itempath, action.payload.state) + const children = layout?.props?.children + const basePath = [...actionPath, 'props', 'children'] + if (Array.isArray(children)) { + children.forEach((v, i) => { + state = adjustHashes(state, { payload: { itempath: [...basePath, i] } }); + }) + } else if (children) { + state = adjustHashes(state, { payload: { itempath: basePath } }); + } + + } + return state +} + function layoutHashes(state = {}, action) { if ( includes(action.type, [ @@ -37,9 +60,7 @@ 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); + return adjustHashes(state, action); } return state; } diff --git a/dash/dash-renderer/src/utils/clientsideFunctions.ts b/dash/dash-renderer/src/utils/clientsideFunctions.ts index f266c72790..168c9a968d 100644 --- a/dash/dash-renderer/src/utils/clientsideFunctions.ts +++ b/dash/dash-renderer/src/utils/clientsideFunctions.ts @@ -16,8 +16,9 @@ function set_props( for (let y = 0; y < ds.length; y++) { const {dispatch, getState} = ds[y]; let componentPath; + const _state = getState(); + const {paths} = _state; if (!Array.isArray(idOrPath)) { - const {paths} = getState(); componentPath = getPath(paths, idOrPath); } else { componentPath = idOrPath; @@ -25,7 +26,8 @@ function set_props( dispatch( updateProps({ props, - itempath: componentPath + itempath: componentPath, + state: _state }) ); dispatch(notifyObservers({id: idOrPath, props})); diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index d8b9600bea..5b8378abe4 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -112,7 +112,8 @@ function DashWrapper({ dispatch( updateProps({ props: changedProps, - itempath: componentPath + itempath: componentPath, + state: currentState }) ); }); @@ -141,7 +142,7 @@ function DashWrapper({ ); const wrapChildrenProp = useCallback( - (node: any, childrenProp: DashLayoutPath) => { + (node: any, childrenPath: DashLayoutPath) => { if (Array.isArray(node)) { return node.map((n, i) => { if (isDryComponent(n)) { @@ -149,7 +150,7 @@ function DashWrapper({ n, concat(componentPath, [ 'props', - ...childrenProp, + ...childrenPath, i ]), i @@ -163,7 +164,7 @@ function DashWrapper({ } return createContainer( node, - concat(componentPath, ['props', ...childrenProp]) + concat(componentPath, ['props', ...childrenPath]) ); }, [componentPath] @@ -376,7 +377,6 @@ function DashWrapper({ component, hydratedProps, extraProps, - wrapChildrenProp, componentProps, h } @@ -384,14 +384,12 @@ function DashWrapper({ }, [element, component, hydratedProps, - wrapChildrenProp, componentProps, config.props_check, h ]) const hydrated = useMemo(() => { - console.log('rendering') let hydratedChildren: any; if (componentProps.children !== undefined) { hydratedChildren = wrapChildrenProp(componentProps.children, [ diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index 040b808877..cd38ac445f 100644 --- a/dash/dash-renderer/src/wrapper/selectors.ts +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -3,25 +3,6 @@ import {getComponentLayout, stringifyPath} from './wrapping'; type SelectDashProps = [DashComponent, BaseDashProps, number]; -const isFirstLevelPropsChild = (updatedPath: string, strPath: string) => { - const updatedSegments = updatedPath.split(','); - const fullSegments = strPath.split(','); - - // Check that strPath actually starts with updatedPath - const startsWithPath = updatedSegments.every( - (seg, i) => fullSegments[i] === seg - ); - - if (!startsWithPath) return false; - - // Get the remaining path after the prefix - const remainingSegments = fullSegments.slice(updatedSegments.length); - - const propsCount = remainingSegments.filter(s => s === 'props').length; - - return propsCount < 2; -} - export const selectDashProps = (componentPath: DashLayoutPath) => (state: any): SelectDashProps => { @@ -31,16 +12,9 @@ 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]) => - { - return isFirstLevelPropsChild(updatedPath, strPath) - ? (pathHash as number) + acc - : acc - }, - 0 - ); - return [c, c.props, h]; + const h = state.layoutHashes[strPath] + + return [c, c?.props, h]; }; export function selectDashPropsEqualityFn( @@ -48,7 +22,7 @@ export function selectDashPropsEqualityFn( [___, ____, previousHash]: SelectDashProps ) { // Only need to compare the hash as any change is summed up - return hash === previousHash; + return (hash === previousHash); } export function selectConfig(state: any) { From dc2e2bdb521a3bde7f08db344c65af5ebd233128 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Thu, 27 Mar 2025 11:59:55 -0400 Subject: [PATCH 04/33] making json stringify simpler to keep from circular references --- dash/dash-renderer/src/wrapper/DashWrapper.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 5b8378abe4..faa3db2277 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -373,19 +373,14 @@ function DashWrapper({ const dependenciesStable = useMemo(() => { return JSON.stringify( { - element, - component, - hydratedProps, - extraProps, - componentProps, - h + h, + componentPath, + componentProps } ) - }, [element, - component, - hydratedProps, + }, [ componentProps, - config.props_check, + componentPath, h ]) From adbb03036e360200b37b8154e73bfc8016161cc7 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:13:31 -0400 Subject: [PATCH 05/33] testing internal `setProps` to make sure the component is still in the dash layout --- dash/dash-renderer/src/wrapper/DashWrapper.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index faa3db2277..e61cf87a30 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -68,11 +68,13 @@ function DashWrapper({ dispatch((dispatch, getState) => { const currentState = getState(); const {graphs} = currentState; - - const {props: oldProps} = getComponentLayout( + 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 From b77e7b76f6068fb4f7dab7592311aaa092d8dd0c Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:49:25 -0400 Subject: [PATCH 06/33] fixing issue with `set_progress` not triggering properly when needing to update `children` --- dash/dash-renderer/src/actions/callbacks.ts | 4 +++- dash/dash-renderer/src/reducers/reducer.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index a61dfd6159..c9e2297669 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -336,6 +336,7 @@ async function handleClientside( function updateComponent(component_id: any, props: any, cb: ICallbackPayload) { return function (dispatch: any, getState: any) { + const _state = getState() const {paths, config} = getState(); const componentPath = getPath(paths, component_id); if (!componentPath) { @@ -360,7 +361,8 @@ function updateComponent(component_id: any, props: any, cb: ICallbackPayload) { dispatch( updateProps({ props, - itempath: componentPath + itempath: componentPath, + state: _state }) ); dispatch(notifyObservers({id: component_id, props})); diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index 76128eba1c..762e90643c 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -34,7 +34,7 @@ function adjustHashes(state, action) { state = assoc(strPath, prev + 1, state); // check if children was adjusted - if ('children' in pathOr({}, ['payload', 'props'], action)) { + if ('children' in pathOr({}, ['payload', 'props'], action) && action.payload?.state) { const layout = getComponentLayout(action.payload.itempath, action.payload.state) const children = layout?.props?.children const basePath = [...actionPath, 'props', 'children'] From 5eed5fee30822bbdb0f38d8a16856bec66f09262 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Thu, 27 Mar 2025 16:58:41 -0400 Subject: [PATCH 07/33] refactor for `dashWrapper` made the `reducer` recursive to get the new children and generate hashes for them --- dash/dash-renderer/src/reducers/reducer.js | 14 ++-- .../dash-renderer/src/wrapper/DashWrapper.tsx | 66 ++++++++++++------- dash/dash-renderer/src/wrapper/selectors.ts | 4 +- 3 files changed, 52 insertions(+), 32 deletions(-) diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index 762e90643c..23fd86aab0 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -1,5 +1,6 @@ import {forEach, includes, isEmpty, keys, path, assoc, pathOr} from 'ramda'; import {combineReducers} from 'redux'; +import {batch} from 'react-redux'; import {getCallbacksByInput} from '../actions/dependencies_ts'; @@ -34,23 +35,22 @@ function adjustHashes(state, action) { state = assoc(strPath, prev + 1, state); // check if children was adjusted - if ('children' in pathOr({}, ['payload', 'props'], action) && action.payload?.state) { - const layout = getComponentLayout(action.payload.itempath, action.payload.state) - const children = layout?.props?.children + if ('children' in pathOr({}, ['payload', 'props'], action)) { + const children = pathOr({}, ['payload', 'props', 'children'], action) const basePath = [...actionPath, 'props', 'children'] if (Array.isArray(children)) { children.forEach((v, i) => { - state = adjustHashes(state, { payload: { itempath: [...basePath, i] } }); + state = adjustHashes(state, { payload: { itempath: [...basePath, i], props: v?.props } }); }) } else if (children) { - state = adjustHashes(state, { payload: { itempath: basePath } }); + state = adjustHashes(state, { payload: { itempath: basePath }, props: children?.props }); } } return state } -function layoutHashes(state = {}, action) { +const layoutHashes = batch(() => (state = {}, action) => { if ( includes(action.type, [ 'UNDO_PROP_CHANGE', @@ -63,7 +63,7 @@ function layoutHashes(state = {}, action) { return adjustHashes(state, action); } return state; -} +}) function mainReducer() { const parts = { diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index e61cf87a30..57cb756d94 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, {useMemo, useCallback, memo, useState, useEffect} from 'react'; import { path, concat, @@ -43,12 +43,19 @@ type DashWrapperProps = { _dashprivate_error?: any; }; +// Define the type for your state +type HydratedState = React.DetailedReactHTMLElement< + React.InputHTMLAttributes, + HTMLInputElement +> | null; + function DashWrapper({ componentPath, _dashprivate_error, ...extras }: DashWrapperProps) { const dispatch = useDispatch(); + const [hydrated, setHydrated] = useState(null); // Get the config for the component as props const config: DashConfig = useSelector(selectConfig); @@ -59,6 +66,22 @@ function DashWrapper({ selectDashPropsEqualityFn ); + const dependenciesStable = useMemo(() => { + return JSON.stringify( + { + h, + componentPath, + componentProps + } + ) + }, [ + JSON.stringify({ + componentProps, + componentPath, + h + }) + ]) + const setProps = (newProps: UpdatePropsPayload) => { const {id} = componentProps; const {_dash_error, ...restProps} = newProps; @@ -177,13 +200,11 @@ function DashWrapper({ ...extras }; - const element = useMemo(() => Registry.resolve(component), [component]); - - const hydratedProps = useMemo(() => { + const setHydratedProps = () => { // 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); @@ -370,23 +391,15 @@ function DashWrapper({ props.id = stringifyId(props.id); } return props; - }, [componentProps]); - - const dependenciesStable = useMemo(() => { - return JSON.stringify( - { - h, - componentPath, - componentProps - } - ) - }, [ - componentProps, - componentPath, - h - ]) + }; - const hydrated = useMemo(() => { + useEffect(() => { + let comp; + if (!component) { + return + } + const element = Registry.resolve(component); + const hydratedProps = setHydratedProps() let hydratedChildren: any; if (componentProps.children !== undefined) { hydratedChildren = wrapChildrenProp(componentProps.children, [ @@ -394,7 +407,7 @@ function DashWrapper({ ]); } if (config.props_check) { - return ( + comp = ( - {hydrated} + {hydrated ? hydrated : <>} ); diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index cd38ac445f..772cbb54ee 100644 --- a/dash/dash-renderer/src/wrapper/selectors.ts +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -13,7 +13,9 @@ export const selectDashProps = const strPath = stringifyPath(componentPath); const h = state.layoutHashes[strPath] - + if (!c) { + return [c, {}, -100] + } return [c, c?.props, h]; }; From 02bf1c8af2fd04ab6c437ec6ce100821d45e5bda Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Fri, 28 Mar 2025 05:48:41 -0400 Subject: [PATCH 08/33] reverting to using memo, but only memoizing on the hash --- .../dash-renderer/src/wrapper/DashWrapper.tsx | 75 ++++++------------- 1 file changed, 21 insertions(+), 54 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 57cb756d94..defe3b92b5 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, useState, useEffect} from 'react'; +import React, {useCallback, memo, useMemo} from 'react'; import { path, concat, @@ -43,11 +43,6 @@ type DashWrapperProps = { _dashprivate_error?: any; }; -// Define the type for your state -type HydratedState = React.DetailedReactHTMLElement< - React.InputHTMLAttributes, - HTMLInputElement -> | null; function DashWrapper({ componentPath, @@ -55,7 +50,6 @@ function DashWrapper({ ...extras }: DashWrapperProps) { const dispatch = useDispatch(); - const [hydrated, setHydrated] = useState(null); // Get the config for the component as props const config: DashConfig = useSelector(selectConfig); @@ -66,22 +60,6 @@ function DashWrapper({ selectDashPropsEqualityFn ); - const dependenciesStable = useMemo(() => { - return JSON.stringify( - { - h, - componentPath, - componentProps - } - ) - }, [ - JSON.stringify({ - componentProps, - componentPath, - h - }) - ]) - const setProps = (newProps: UpdatePropsPayload) => { const {id} = componentProps; const {_dash_error, ...restProps} = newProps; @@ -393,44 +371,33 @@ function DashWrapper({ return props; }; - useEffect(() => { - let comp; + const hydrated = useMemo(() => { if (!component) { - return + return; } + const element = Registry.resolve(component); - const hydratedProps = setHydratedProps() + const hydratedProps = setHydratedProps(); + let hydratedChildren: any; if (componentProps.children !== undefined) { - hydratedChildren = wrapChildrenProp(componentProps.children, [ - 'children' - ]); - } - if (config.props_check) { - comp = ( - - {createElement( - element, - hydratedProps, - extraProps, - hydratedChildren - )} - - ); + hydratedChildren = wrapChildrenProp(componentProps.children, ['children']); } - comp = createElement( - element, - hydratedProps, - extraProps, - hydratedChildren + const rendered = config.props_check ? ( + + {createElement(element, hydratedProps, extraProps, hydratedChildren)} + + ) : ( + createElement(element, hydratedProps, extraProps, hydratedChildren) ); - setHydrated(comp) - }, [dependenciesStable]); + + return rendered + }, [h]); if (!component) { return null @@ -448,7 +415,7 @@ function DashWrapper({ dispatch={dispatch} > - {hydrated ? hydrated : <>} + {hydrated ? hydrated :
} ); From cbc93b965f1a554fb0200a024cc51ecb6acf4c73 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:31:14 -0400 Subject: [PATCH 09/33] adjustments for how children are added when the prop was changed --- dash/dash-renderer/src/actions/callbacks.ts | 7 ++--- .../src/observers/executedCallbacks.ts | 5 ++-- dash/dash-renderer/src/reducers/reducer.js | 26 ++++++++-------- .../src/utils/clientsideFunctions.ts | 3 +- .../dash-renderer/src/wrapper/DashWrapper.tsx | 30 ++++++++++--------- dash/dash-renderer/src/wrapper/selectors.ts | 6 ++-- 6 files changed, 39 insertions(+), 38 deletions(-) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index c9e2297669..d4da0cbb7a 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -336,8 +336,8 @@ async function handleClientside( function updateComponent(component_id: any, props: any, cb: ICallbackPayload) { return function (dispatch: any, getState: any) { - const _state = getState() - const {paths, config} = getState(); + const _state = getState(); + const {paths, config} = _state; const componentPath = getPath(paths, component_id); if (!componentPath) { if (!config.suppress_callback_exceptions) { @@ -361,8 +361,7 @@ function updateComponent(component_id: any, props: any, cb: ICallbackPayload) { dispatch( updateProps({ props, - itempath: componentPath, - state: _state + itempath: componentPath }) ); 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 b8be2e517e..25fde7355f 100644 --- a/dash/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash/dash-renderer/src/observers/executedCallbacks.ts @@ -45,7 +45,7 @@ const observer: IStoreObserverDefinition = { } = getState(); function applyProps(id: any, updatedProps: any) { - const _state = getState() + const _state = getState(); const {layout, paths} = _state; const itempath = getPath(paths, id); if (!itempath) { @@ -69,8 +69,7 @@ const observer: IStoreObserverDefinition = { updateProps({ itempath, props, - source: 'response', - state: _state + source: 'response' }) ); diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index 23fd86aab0..fd0281c495 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -1,6 +1,5 @@ import {forEach, includes, isEmpty, keys, path, assoc, pathOr} from 'ramda'; import {combineReducers} from 'redux'; -import {batch} from 'react-redux'; import {getCallbacksByInput} from '../actions/dependencies_ts'; @@ -19,7 +18,7 @@ import layout from './layout'; import paths from './paths'; import callbackJobs from './callbackJobs'; import loading from './loading'; -import {stringifyPath, getComponentLayout} from '../wrapper/wrapping'; +import {stringifyPath} from '../wrapper/wrapping'; export const apiRequests = [ 'dependenciesRequest', @@ -29,28 +28,31 @@ export const apiRequests = [ ]; function adjustHashes(state, action) { - const actionPath = action.payload.itempath + const actionPath = action.payload.itempath; const strPath = stringifyPath(actionPath); const prev = pathOr(0, [strPath], state); state = assoc(strPath, prev + 1, state); // check if children was adjusted if ('children' in pathOr({}, ['payload', 'props'], action)) { - const children = pathOr({}, ['payload', 'props', 'children'], action) - const basePath = [...actionPath, 'props', 'children'] + const children = pathOr({}, ['payload', 'props', 'children'], action); + const basePath = [...actionPath, 'props', 'children']; if (Array.isArray(children)) { children.forEach((v, i) => { - state = adjustHashes(state, { payload: { itempath: [...basePath, i], props: v?.props } }); - }) + state = adjustHashes(state, { + payload: {itempath: [...basePath, i], props: v?.props} + }); + }); } else if (children) { - state = adjustHashes(state, { payload: { itempath: basePath }, props: children?.props }); + state = adjustHashes(state, { + payload: {itempath: [...basePath], props: children?.props} + }); } - } - return state + return state; } -const layoutHashes = batch(() => (state = {}, action) => { +const layoutHashes = (state = {}, action) => { if ( includes(action.type, [ 'UNDO_PROP_CHANGE', @@ -63,7 +65,7 @@ const layoutHashes = batch(() => (state = {}, action) => { return adjustHashes(state, action); } 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 168c9a968d..0e7f9a0759 100644 --- a/dash/dash-renderer/src/utils/clientsideFunctions.ts +++ b/dash/dash-renderer/src/utils/clientsideFunctions.ts @@ -26,8 +26,7 @@ function set_props( dispatch( updateProps({ props, - itempath: componentPath, - state: _state + itempath: componentPath }) ); dispatch(notifyObservers({id: idOrPath, props})); diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index defe3b92b5..3989441621 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -43,7 +43,6 @@ type DashWrapperProps = { _dashprivate_error?: any; }; - function DashWrapper({ componentPath, _dashprivate_error, @@ -69,13 +68,10 @@ function DashWrapper({ dispatch((dispatch, getState) => { const currentState = getState(); const {graphs} = currentState; - const oldLayout = getComponentLayout( - componentPath, - currentState - ) - if (!oldLayout) return + const oldLayout = getComponentLayout(componentPath, currentState); + if (!oldLayout) return; const {props: oldProps} = oldLayout; - if (!oldProps) return + if (!oldProps) return; const changedProps = pickBy( (val, key) => !equals(val, oldProps[key]), restProps @@ -115,8 +111,7 @@ function DashWrapper({ dispatch( updateProps({ props: changedProps, - itempath: componentPath, - state: currentState + itempath: componentPath }) ); }); @@ -381,7 +376,9 @@ function DashWrapper({ let hydratedChildren: any; if (componentProps.children !== undefined) { - hydratedChildren = wrapChildrenProp(componentProps.children, ['children']); + hydratedChildren = wrapChildrenProp(componentProps.children, [ + 'children' + ]); } const rendered = config.props_check ? ( @@ -390,17 +387,22 @@ function DashWrapper({ props={hydratedProps} component={component} > - {createElement(element, hydratedProps, extraProps, hydratedChildren)} + {createElement( + element, + hydratedProps, + extraProps, + hydratedChildren + )} ) : ( createElement(element, hydratedProps, extraProps, hydratedChildren) ); - return rendered + return rendered; }, [h]); if (!component) { - return null + return null; } return ( @@ -415,7 +417,7 @@ function DashWrapper({ dispatch={dispatch} > - {hydrated ? hydrated :
} + {hydrated ? hydrated :
} ); diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index 772cbb54ee..2ce0fa679f 100644 --- a/dash/dash-renderer/src/wrapper/selectors.ts +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -12,9 +12,9 @@ export const selectDashProps = // Then it can be easily compared without having to compare the props. const strPath = stringifyPath(componentPath); - const h = state.layoutHashes[strPath] + const h = state.layoutHashes[strPath]; if (!c) { - return [c, {}, -100] + return [c, {}, -100]; } return [c, c?.props, h]; }; @@ -24,7 +24,7 @@ export function selectDashPropsEqualityFn( [___, ____, previousHash]: SelectDashProps ) { // Only need to compare the hash as any change is summed up - return (hash === previousHash); + return hash === previousHash; } export function selectConfig(state: any) { From 8ae3b22ec55227e1225f8c50facd8814953aeb8c Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Fri, 28 Mar 2025 17:13:19 -0400 Subject: [PATCH 10/33] adding support for sending hashes to components as props adjusted the components as props test to work with redrawing --- dash/dash-renderer/src/actions/callbacks.ts | 6 +- .../src/observers/executedCallbacks.ts | 9 +- dash/dash-renderer/src/reducers/reducer.js | 183 ++++++++- .../src/utils/clientsideFunctions.ts | 8 +- .../dash-renderer/src/wrapper/DashWrapper.tsx | 4 +- .../renderer/test_component_as_prop.py | 367 +++++++++--------- 6 files changed, 377 insertions(+), 200 deletions(-) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index d4da0cbb7a..5a01958937 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -34,6 +34,7 @@ import { CallbackResponseData, SideUpdateOutput } from '../types/callbacks'; +import {getComponentLayout} from '../wrapper/wrapping'; import {isMultiValued, stringifyId, isMultiOutputProp} from './dependencies'; import {urlBase} from './utils'; import {getCSRFHeader, dispatchError} from '.'; @@ -358,10 +359,13 @@ function updateComponent(component_id: any, props: any, cb: ICallbackPayload) { // error. return; } + const component = getComponentLayout(componentPath, _state); dispatch( updateProps({ props, - itempath: componentPath + itempath: componentPath, + component, + config }) ); 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 25fde7355f..26c4bfdf06 100644 --- a/dash/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash/dash-renderer/src/observers/executedCallbacks.ts @@ -34,6 +34,7 @@ import {ICallback, IStoredCallback} from '../types/callbacks'; import {updateProps, setPaths, handleAsyncError} from '../actions'; import {getPath, computePaths} from '../actions/paths'; +import {getComponentLayout} from '../wrapper/wrapping'; import {applyPersistence, prunePersistence} from '../persistence'; import {IStoreObserverDefinition} from '../StoreObserver'; @@ -46,7 +47,7 @@ const observer: IStoreObserverDefinition = { function applyProps(id: any, updatedProps: any) { const _state = getState(); - const {layout, paths} = _state; + const {layout, paths, config} = _state; const itempath = getPath(paths, id); if (!itempath) { return false; @@ -64,12 +65,14 @@ 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); - + const component = getComponentLayout(itempath, _state); dispatch( updateProps({ itempath, props, - source: 'response' + source: 'response', + component, + config }) ); diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index fd0281c495..3206ebcbec 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -27,28 +27,177 @@ export const apiRequests = [ 'loginRequest' ]; +function handleChildrenPropsUpdate({ + component, + config, + action, + actionPath, + state +}) { + const childrenProps = pathOr( + [], + ['children_props', component?.namespace, component?.type], + config + ); + + // Ensure "children" is always considered + if (!childrenProps.includes('children[]')) { + childrenProps.push('children[]'); + } + + childrenProps.forEach(childrenProp => { + const segments = childrenProp.split('.'); + const includesArray = childrenProp.includes('[]'); + const includesObject = childrenProp.includes('{}'); + + const cleanSegments = segments.map(s => + s.replace('[]', '').replace('{}', '') + ); + + const getFrontBack = () => { + const front = []; + const back = []; + let found = false; + + for (const segment of segments) { + const clean = segment.replace('{}', '').replace('[]', ''); + if ( + !found && + (segment.includes('[]') || segment.includes('{}')) + ) { + found = true; + front.push(clean); + } else if (found) { + back.push(clean); + } else { + front.push(clean); + } + } + + return [front, back]; + }; + + const [frontPath, backPath] = getFrontBack(); + const basePath = [...actionPath, 'props', ...frontPath]; + const propRoot = pathOr({}, ['payload', 'props'], action); + + if (!(cleanSegments[0] in propRoot)) return; + + const _fullValue = path(cleanSegments, propRoot); + const fullValues = Array.isArray(_fullValue) + ? _fullValue + : [_fullValue]; + + fullValues.forEach((fullValue, y) => { + if (includesArray) { + if (Array.isArray(fullValue)) { + fullValue.forEach((el, i) => { + let value = el; + if (includesObject && backPath.length) { + value = path(backPath, el); + } + + if (value) { + const itempath = [...basePath, i, ...backPath]; + state = adjustHashes(state, { + payload: { + itempath, + props: value?.props, + component: value, + config + } + }); + } + }); + } else if ( + fullValue && + typeof fullValue === 'object' && + !('props' in fullValue) + ) { + Object.entries(fullValue).forEach(([key, value]) => { + const finalVal = backPath.length + ? path(backPath, value) + : value; + if (finalVal) { + const itempath = [...basePath, y, key, ...backPath]; + state = adjustHashes(state, { + payload: { + itempath, + props: finalVal?.props, + component: finalVal, + config + } + }); + } + }); + } else if (fullValue) { + const itempath = [...basePath, ...backPath]; + if (Array.isArray(_fullValue)) { + itempath.push(y); + } + state = adjustHashes(state, { + payload: { + itempath, + props: fullValue?.props, + component: fullValue, + config + } + }); + } + } else if (includesObject) { + if (fullValue && typeof fullValue === 'object') { + Object.entries(fullValue).forEach(([key, value]) => { + const finalVal = backPath.length + ? path(backPath, value) + : value; + if (finalVal) { + const itempath = [...basePath, key, ...backPath]; + state = adjustHashes(state, { + payload: { + itempath, + props: finalVal?.props, + component: finalVal, + config + } + }); + } + }); + } + } else { + if (fullValue) { + const itempath = [...actionPath, 'props', ...cleanSegments]; + if (Array.isArray(_fullValue)) { + itempath.push(y); + } + state = adjustHashes(state, { + payload: { + itempath, + props: fullValue?.props, + component: fullValue, + config + } + }); + } + } + }); + }); + + return state; +} + function adjustHashes(state, action) { const actionPath = action.payload.itempath; const strPath = stringifyPath(actionPath); const prev = pathOr(0, [strPath], state); + const {component, config} = action.payload; state = assoc(strPath, prev + 1, state); - - // check if children was adjusted - if ('children' in pathOr({}, ['payload', 'props'], action)) { - const children = pathOr({}, ['payload', 'props', 'children'], action); - const basePath = [...actionPath, 'props', 'children']; - if (Array.isArray(children)) { - children.forEach((v, i) => { - state = adjustHashes(state, { - payload: {itempath: [...basePath, i], props: v?.props} - }); - }); - } else if (children) { - state = adjustHashes(state, { - payload: {itempath: [...basePath], props: children?.props} - }); - } - } + state = handleChildrenPropsUpdate({ + component, + config, + action, + actionPath, + state + }); return state; } diff --git a/dash/dash-renderer/src/utils/clientsideFunctions.ts b/dash/dash-renderer/src/utils/clientsideFunctions.ts index 0e7f9a0759..1f4fd16842 100644 --- a/dash/dash-renderer/src/utils/clientsideFunctions.ts +++ b/dash/dash-renderer/src/utils/clientsideFunctions.ts @@ -1,6 +1,7 @@ import {updateProps, notifyObservers} from '../actions/index'; import {getPath} from '../actions/paths'; import {getStores} from './stores'; +import {getComponentLayout} from '../wrapper/wrapping'; /** * Set the props of a dash component by id or path. @@ -17,16 +18,19 @@ function set_props( const {dispatch, getState} = ds[y]; let componentPath; const _state = getState(); - const {paths} = _state; + const {paths, config} = _state; if (!Array.isArray(idOrPath)) { componentPath = getPath(paths, idOrPath); } else { componentPath = idOrPath; } + const component = getComponentLayout(componentPath, _state); dispatch( updateProps({ props, - itempath: componentPath + itempath: componentPath, + component, + config }) ); dispatch(notifyObservers({id: idOrPath, props})); diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 3989441621..70e2dc1c24 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -111,7 +111,9 @@ function DashWrapper({ dispatch( updateProps({ props: changedProps, - itempath: componentPath + itempath: componentPath, + component, + config }) ); }); 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 From 5c505e987096d9b54657fc0d0fe9e5d00a4a5b2f Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Fri, 28 Mar 2025 17:34:05 -0400 Subject: [PATCH 11/33] adjusting children for rerendering --- dash/dash-renderer/src/reducers/reducer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index 3206ebcbec..b7bb1481f4 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -41,8 +41,8 @@ function handleChildrenPropsUpdate({ ); // Ensure "children" is always considered - if (!childrenProps.includes('children[]')) { - childrenProps.push('children[]'); + if (!childrenProps.includes('children')) { + childrenProps.push('children'); } childrenProps.forEach(childrenProp => { From 3d4ec1dc2091fcd0c8cd81cd1793dd7ab53972ae Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Fri, 28 Mar 2025 17:36:46 -0400 Subject: [PATCH 12/33] adding changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36f866ecd3..e0a892a729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ 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] changes to the rendering logic ## [3.0.1] - 2025-03-24 From c77212642de29cccee74b8c29721f65adc562fe6 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Sat, 29 Mar 2025 01:32:07 -0400 Subject: [PATCH 13/33] updated wrapper to pass new props down to the components upon a new render or the target prop being adjusted vs having to crawl with the reducer the layouthash now has the props that triggered the change as well as the number of updates --- dash/dash-renderer/src/reducers/reducer.js | 174 +-------------- .../dash-renderer/src/wrapper/DashWrapper.tsx | 201 ++++++++++++++---- dash/dash-renderer/src/wrapper/selectors.ts | 13 +- 3 files changed, 171 insertions(+), 217 deletions(-) diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index b7bb1481f4..f566927a19 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -27,177 +27,17 @@ export const apiRequests = [ 'loginRequest' ]; -function handleChildrenPropsUpdate({ - component, - config, - action, - actionPath, - state -}) { - const childrenProps = pathOr( - [], - ['children_props', component?.namespace, component?.type], - config - ); - - // Ensure "children" is always considered - if (!childrenProps.includes('children')) { - childrenProps.push('children'); - } - - childrenProps.forEach(childrenProp => { - const segments = childrenProp.split('.'); - const includesArray = childrenProp.includes('[]'); - const includesObject = childrenProp.includes('{}'); - - const cleanSegments = segments.map(s => - s.replace('[]', '').replace('{}', '') - ); - - const getFrontBack = () => { - const front = []; - const back = []; - let found = false; - - for (const segment of segments) { - const clean = segment.replace('{}', '').replace('[]', ''); - if ( - !found && - (segment.includes('[]') || segment.includes('{}')) - ) { - found = true; - front.push(clean); - } else if (found) { - back.push(clean); - } else { - front.push(clean); - } - } - - return [front, back]; - }; - - const [frontPath, backPath] = getFrontBack(); - const basePath = [...actionPath, 'props', ...frontPath]; - const propRoot = pathOr({}, ['payload', 'props'], action); - - if (!(cleanSegments[0] in propRoot)) return; - - const _fullValue = path(cleanSegments, propRoot); - const fullValues = Array.isArray(_fullValue) - ? _fullValue - : [_fullValue]; - - fullValues.forEach((fullValue, y) => { - if (includesArray) { - if (Array.isArray(fullValue)) { - fullValue.forEach((el, i) => { - let value = el; - if (includesObject && backPath.length) { - value = path(backPath, el); - } - - if (value) { - const itempath = [...basePath, i, ...backPath]; - state = adjustHashes(state, { - payload: { - itempath, - props: value?.props, - component: value, - config - } - }); - } - }); - } else if ( - fullValue && - typeof fullValue === 'object' && - !('props' in fullValue) - ) { - Object.entries(fullValue).forEach(([key, value]) => { - const finalVal = backPath.length - ? path(backPath, value) - : value; - if (finalVal) { - const itempath = [...basePath, y, key, ...backPath]; - state = adjustHashes(state, { - payload: { - itempath, - props: finalVal?.props, - component: finalVal, - config - } - }); - } - }); - } else if (fullValue) { - const itempath = [...basePath, ...backPath]; - if (Array.isArray(_fullValue)) { - itempath.push(y); - } - state = adjustHashes(state, { - payload: { - itempath, - props: fullValue?.props, - component: fullValue, - config - } - }); - } - } else if (includesObject) { - if (fullValue && typeof fullValue === 'object') { - Object.entries(fullValue).forEach(([key, value]) => { - const finalVal = backPath.length - ? path(backPath, value) - : value; - if (finalVal) { - const itempath = [...basePath, key, ...backPath]; - state = adjustHashes(state, { - payload: { - itempath, - props: finalVal?.props, - component: finalVal, - config - } - }); - } - }); - } - } else { - if (fullValue) { - const itempath = [...actionPath, 'props', ...cleanSegments]; - if (Array.isArray(_fullValue)) { - itempath.push(y); - } - state = adjustHashes(state, { - payload: { - itempath, - props: fullValue?.props, - component: fullValue, - config - } - }); - } - } - }); - }); - - return state; -} - function adjustHashes(state, action) { const actionPath = action.payload.itempath; const strPath = stringifyPath(actionPath); - const prev = pathOr(0, [strPath], state); - const {component, config} = action.payload; - state = assoc(strPath, prev + 1, state); - state = handleChildrenPropsUpdate({ - component, - config, - action, - actionPath, + const prev = pathOr('0', [strPath], state); + state = assoc( + strPath, + `${parseInt(prev.split(' -')[0]) + 1} - ${JSON.stringify( + action.payload.props + )}`, state - }); + ); return state; } diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 70e2dc1c24..63d1472a2c 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, memo, useMemo} from 'react'; +import React, {useCallback, MutableRefObject, useRef, useMemo} from 'react'; import { path, concat, @@ -14,7 +14,11 @@ import { dissoc, assoc, mapObjIndexed, - type + type, + reduce, + difference, + intersection, + filter } from 'ramda'; import {useSelector, useDispatch, batch} from 'react-redux'; @@ -35,30 +39,109 @@ import { import CheckedComponent from './CheckedComponent'; import {DashContextProvider} from './DashContext'; +// Define types for the input objects and the output differences +type Dictionary = Record; + +interface Difference { + obj1: any; + obj2: any; +} + +const findDifferences = ( + obj1: Dictionary, + obj2: Dictionary +): Record => { + const commonKeys = intersection(keys(obj1), keys(obj2)); + + const differingKeys = filter( + (key: string) => !equals(obj1[key], obj2[key]), + commonKeys + ) as string[]; + + const keysOnlyInObj2 = difference(keys(obj2), keys(obj1)); + + const differences: Record = reduce( + (acc: Record, key: string) => { + acc[key] = {obj1: obj1[key], obj2: obj2[key]}; + return acc; + }, + {}, + differingKeys as string[] + ); + + keysOnlyInObj2.forEach(key => { + differences[key] = {obj1: undefined, obj2: obj2[key]}; + }); + + return differences; +}; + type DashWrapperProps = { /** * Path of the component in the layout. */ 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. +}; + +// Define a type for the memoized props +type MemoizedPropsType = { + [key: string]: any; }; function DashWrapper({ componentPath, _dashprivate_error, + _passedComponent, + _newRender, ...extras }: DashWrapperProps) { const dispatch = useDispatch(); + const memoizedKeys: MutableRefObject = useRef({}); + const memoizedProps: MutableRefObject = useRef({}); + const newRender = useRef(false); // Get the config for the component as props const config: DashConfig = useSelector(selectConfig); // Select both the component and it's props. - const [component, componentProps, h] = useSelector( + // eslint-disable-next-line prefer-const + let [component, componentProps, h, changedProps] = useSelector( selectDashProps(componentPath), selectDashPropsEqualityFn ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const newlyRendered = useMemo(() => { + if (_newRender) { + memoizedProps.current = {}; + newRender.current = true; + h = 0; + if (h in memoizedKeys.current) { + delete memoizedKeys.current[h]; + } + } else { + newRender.current = false; + } + return newRender.current; + }, [_newRender]); + + let propDifferences: any = {}; + if (memoizedProps.current) { + propDifferences = findDifferences( + memoizedProps.current, + componentProps + ); + } + memoizedProps.current = componentProps; + const setProps = (newProps: UpdatePropsPayload) => { const {id} = componentProps; const {_dash_error, ...restProps} = newProps; @@ -121,7 +204,7 @@ function DashWrapper({ }; const createContainer = useCallback( - (container, containerPath, key = undefined) => { + (container, containerPath, _childNewRender, key = undefined) => { if (isSimpleComponent(component)) { return component; } @@ -135,6 +218,8 @@ function DashWrapper({ } _dashprivate_error={_dashprivate_error} componentPath={containerPath} + _passedComponent={container} + _newRender={_childNewRender} /> ); }, @@ -142,7 +227,7 @@ function DashWrapper({ ); const wrapChildrenProp = useCallback( - (node: any, childrenPath: DashLayoutPath) => { + (node: any, childrenPath: DashLayoutPath, _childNewRender: any) => { if (Array.isArray(node)) { return node.map((n, i) => { if (isDryComponent(n)) { @@ -153,6 +238,7 @@ function DashWrapper({ ...childrenPath, i ]), + _childNewRender, i ); } @@ -164,7 +250,8 @@ function DashWrapper({ } return createContainer( node, - concat(componentPath, ['props', ...childrenPath]) + concat(componentPath, ['props', ...childrenPath]), + _childNewRender ); }, [componentPath] @@ -175,7 +262,7 @@ function DashWrapper({ ...extras }; - const setHydratedProps = () => { + const setHydratedProps = (component: any, componentProps: any) => { // Hydrate components props const childrenProps = pathOr( [], @@ -186,10 +273,24 @@ function DashWrapper({ for (let i = 0; i < childrenProps.length; i++) { const childrenProp: string = childrenProps[i]; - + let childNewRender = 0; + if ( + childrenProp + .split('.')[0] + .replace('[]', '') + .replace('{}', '') in propDifferences || + childrenProp + .split('.')[0] + .replace('[]', '') + .replace('{}', '') in changedProps || + newRender.current + ) { + childNewRender = Date.now(); + } const handleObject = (obj: any, opath: DashLayoutPath) => { return mapObjIndexed( - (node, k) => wrapChildrenProp(node, [...opath, k]), + (node, k) => + wrapChildrenProp(node, [...opath, k], childNewRender), obj ); }; @@ -259,7 +360,8 @@ function DashWrapper({ } else { listValue = wrapChildrenProp( path(backPath, el), - elementPath + elementPath, + childNewRender ); } return assocPath(backPath, listValue, el); @@ -305,7 +407,8 @@ function DashWrapper({ dynamic, concat([k], backPath) ) - : concat(dynamic, [k]) + : concat(dynamic, [k]), + childNewRender ), dynValue ); @@ -316,7 +419,11 @@ function DashWrapper({ if (node === undefined) { continue; } - nodeValue = wrapChildrenProp(node, childrenPath); + nodeValue = wrapChildrenProp( + node, + childrenPath, + childNewRender + ); } } props = assocPath(childrenPath, nodeValue, props); @@ -352,7 +459,11 @@ function DashWrapper({ if (node !== undefined) { props = assoc( childrenProp, - wrapChildrenProp(node, [childrenProp]), + wrapChildrenProp( + node, + [childrenProp], + childNewRender + ), props ); } @@ -368,20 +479,31 @@ function DashWrapper({ return props; }; - const hydrated = useMemo(() => { + const hydrateFunc = () => { + if (newRender.current) { + component = _passedComponent; + componentProps = _passedComponent?.props; + } if (!component) { - return; + return null; } const element = Registry.resolve(component); - const hydratedProps = setHydratedProps(); + const hydratedProps = setHydratedProps(component, componentProps); let hydratedChildren: any; if (componentProps.children !== undefined) { - hydratedChildren = wrapChildrenProp(componentProps.children, [ - 'children' - ]); + hydratedChildren = wrapChildrenProp( + componentProps.children, + ['children'], + 'children' in propDifferences || + newRender.current || + 'children' in changedProps + ? Date.now() + : 0 + ); } + newRender.current = false; const rendered = config.props_check ? ( - {hydrated ? 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 2ce0fa679f..fa12082fc9 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]; export const selectDashProps = (componentPath: DashLayoutPath) => @@ -12,11 +12,14 @@ export const selectDashProps = // Then it can be easily compared without having to compare the props. const strPath = stringifyPath(componentPath); - const h = state.layoutHashes[strPath]; - if (!c) { - return [c, {}, -100]; + const hash = state.layoutHashes[strPath]; + let h = 0; + let changedProps: object = {}; + if (hash) { + h = parseInt(hash.split(' -')[0]); + changedProps = JSON.parse(hash.split('- ')[1]); } - return [c, c?.props, h]; + return [c, c?.props, h, changedProps]; }; export function selectDashPropsEqualityFn( From 62ec2acbb01dbd03f0e7198bbaf25228072123b0 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Sat, 29 Mar 2025 01:43:47 -0400 Subject: [PATCH 14/33] adding ignore for unused var --- dash/dash-renderer/src/wrapper/DashWrapper.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 63d1472a2c..21a267a346 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -119,6 +119,7 @@ function DashWrapper({ ); // eslint-disable-next-line @typescript-eslint/no-unused-vars + // @ts-ignore const newlyRendered = useMemo(() => { if (_newRender) { memoizedProps.current = {}; From 197901a45428fefed781e601b4a1a33437fce219 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Sat, 29 Mar 2025 02:01:31 -0400 Subject: [PATCH 15/33] swapped hash with actual item and props --- dash/dash-renderer/src/reducers/reducer.js | 6 ++---- dash/dash-renderer/src/wrapper/selectors.ts | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index f566927a19..ae066c7286 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -30,12 +30,10 @@ export const apiRequests = [ function adjustHashes(state, action) { const actionPath = action.payload.itempath; const strPath = stringifyPath(actionPath); - const prev = pathOr('0', [strPath], state); + const prev = pathOr(0, [strPath, 0], state); state = assoc( strPath, - `${parseInt(prev.split(' -')[0]) + 1} - ${JSON.stringify( - action.payload.props - )}`, + [prev + 1, action.payload.props], state ); return state; diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index fa12082fc9..5db8c8d194 100644 --- a/dash/dash-renderer/src/wrapper/selectors.ts +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -16,8 +16,8 @@ export const selectDashProps = let h = 0; let changedProps: object = {}; if (hash) { - h = parseInt(hash.split(' -')[0]); - changedProps = JSON.parse(hash.split('- ')[1]); + h = hash[0]; + changedProps = hash[1]; } return [c, c?.props, h, changedProps]; }; From e2f89e992657d1c3ab228429602aecef7df356a6 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Sat, 29 Mar 2025 02:03:52 -0400 Subject: [PATCH 16/33] adjustments for lint --- dash/dash-renderer/src/wrapper/DashWrapper.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 21a267a346..dc359c1085 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -118,7 +118,7 @@ function DashWrapper({ selectDashPropsEqualityFn ); - // eslint-disable-next-line @typescript-eslint/no-unused-vars + /* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */ // @ts-ignore const newlyRendered = useMemo(() => { if (_newRender) { @@ -133,6 +133,7 @@ function DashWrapper({ } return newRender.current; }, [_newRender]); + /* eslint-enable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */ let propDifferences: any = {}; if (memoizedProps.current) { From 8862956ace5f49306ed68f24d050ba06bbbef344 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Sat, 29 Mar 2025 02:27:12 -0400 Subject: [PATCH 17/33] updated patch test to use until instead of assert to allow timing flux --- dash/dash-renderer/src/reducers/reducer.js | 6 +--- tests/integration/test_patch.py | 39 +++++++++++----------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index ae066c7286..a9ce2384e6 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -31,11 +31,7 @@ function adjustHashes(state, action) { const actionPath = action.payload.itempath; const strPath = stringifyPath(actionPath); const prev = pathOr(0, [strPath, 0], state); - state = assoc( - strPath, - [prev + 1, action.payload.props], - state - ); + state = assoc(strPath, [prev + 1, action.payload.props], state); return state; } diff --git a/tests/integration/test_patch.py b/tests/integration/test_patch.py index 99041a6902..334923da18 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,52 @@ 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") == [ + 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 +273,40 @@ def get_output(): _input.send_keys("-1") dash_duo.find_element("#insert-btn").click() - assert get_output().get("array") == [ + 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") == [ + until(lambda: get_output().get("array") == [ "Prepend", "initial", "Append", "Extend", - ] + ], 2) dash_duo.find_element("#reverse-btn").click() - assert get_output().get("array") == [ + until(lambda: get_output().get("array") == [ "Extend", "Append", "initial", "Prepend", - ] + ], 2) dash_duo.find_element("#remove-btn").click() - assert get_output().get("array") == [ + 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): From 3bc85d3a3cc89a72c4f2be2fd36ede154465afd2 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Sat, 29 Mar 2025 06:39:52 -0400 Subject: [PATCH 18/33] fixing for lint --- tests/integration/test_patch.py | 88 ++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 33 deletions(-) diff --git a/tests/integration/test_patch.py b/tests/integration/test_patch.py index 334923da18..c2b3b3f500 100644 --- a/tests/integration/test_patch.py +++ b/tests/integration/test_patch.py @@ -238,7 +238,9 @@ def get_output(): _input.send_keys("Extend") dash_duo.find_element("#extend-btn").click() - until(lambda: get_output()["array"] == ["Prepend", "initial", "Append", "Extend"], 2) + until( + lambda: get_output()["array"] == ["Prepend", "initial", "Append", "Extend"], 2 + ) undef = object() until(lambda: get_output().get("merge", undef) is undef, 2) @@ -259,13 +261,17 @@ def get_output(): _input.send_keys("Inserted") dash_duo.find_element("#insert-btn").click() - until(lambda: get_output().get("array") == [ - "Prepend", - "Inserted", - "initial", - "Append", - "Extend", - ], 2) + 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") @@ -273,37 +279,53 @@ def get_output(): _input.send_keys("-1") dash_duo.find_element("#insert-btn").click() - until(lambda: get_output().get("array") == [ - "Prepend", - "Inserted", - "initial", - "Append", - "Inserted with negative index", - "Extend", - ], 2) + until( + lambda: get_output().get("array") + == [ + "Prepend", + "Inserted", + "initial", + "Append", + "Inserted with negative index", + "Extend", + ], + 2, + ) dash_duo.find_element("#delete-index").click() - until(lambda: get_output().get("array") == [ - "Prepend", - "initial", - "Append", - "Extend", - ], 2) + until( + lambda: get_output().get("array") + == [ + "Prepend", + "initial", + "Append", + "Extend", + ], + 2, + ) dash_duo.find_element("#reverse-btn").click() - until(lambda: get_output().get("array") == [ - "Extend", - "Append", - "initial", - "Prepend", - ], 2) + until( + lambda: get_output().get("array") + == [ + "Extend", + "Append", + "initial", + "Prepend", + ], + 2, + ) dash_duo.find_element("#remove-btn").click() - until(lambda: get_output().get("array") == [ - "Extend", - "Append", - "Prepend", - ], 2) + until( + lambda: get_output().get("array") + == [ + "Extend", + "Append", + "Prepend", + ], + 2, + ) dash_duo.find_element("#clear-btn").click() until(lambda: get_output()["array"] == [], 2) From 5c6dbc39337cfa21c0a3afe39b6388273d230cda Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 31 Mar 2025 05:26:16 -0400 Subject: [PATCH 19/33] adjusting to only render children components as 'new' if it is a new render, there is no hash or the component prop is in the changedProps. There was an issue if the component props had changed from the children, it would think that the parents props had changed and needed to re-render. --- dash/dash-renderer/src/actions/callbacks.ts | 3 +- .../src/observers/executedCallbacks.ts | 3 +- dash/dash-renderer/src/reducers/reducer.js | 5 +- .../src/utils/clientsideFunctions.ts | 3 +- .../dash-renderer/src/wrapper/DashWrapper.tsx | 65 +++---------------- dash/dash-renderer/src/wrapper/selectors.ts | 10 +-- 6 files changed, 23 insertions(+), 66 deletions(-) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index 5a01958937..917e942ab0 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -365,7 +365,8 @@ function updateComponent(component_id: any, props: any, cb: ICallbackPayload) { props, itempath: componentPath, component, - config + config, + 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 26c4bfdf06..3da7dda3d9 100644 --- a/dash/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash/dash-renderer/src/observers/executedCallbacks.ts @@ -72,7 +72,8 @@ const observer: IStoreObserverDefinition = { props, source: 'response', component, - config + config, + renderType: 'callback' }) ); diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index a9ce2384e6..0cafbd4be8 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -30,8 +30,9 @@ export const apiRequests = [ function adjustHashes(state, action) { const actionPath = action.payload.itempath; const strPath = stringifyPath(actionPath); - const prev = pathOr(0, [strPath, 0], state); - state = assoc(strPath, [prev + 1, action.payload.props], state); + const prev = pathOr(0, [strPath, 'hash'], state); + state = assoc(strPath, {hash: prev + 1, + changedProps: action.payload.props, renderType: action.payload.renderType}, state); return state; } diff --git a/dash/dash-renderer/src/utils/clientsideFunctions.ts b/dash/dash-renderer/src/utils/clientsideFunctions.ts index 1f4fd16842..4c4d4f1428 100644 --- a/dash/dash-renderer/src/utils/clientsideFunctions.ts +++ b/dash/dash-renderer/src/utils/clientsideFunctions.ts @@ -30,7 +30,8 @@ function set_props( props, itempath: componentPath, component, - config + config, + 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 dc359c1085..04846b880a 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -14,11 +14,7 @@ import { dissoc, assoc, mapObjIndexed, - type, - reduce, - difference, - intersection, - filter + type } from 'ramda'; import {useSelector, useDispatch, batch} from 'react-redux'; @@ -39,43 +35,6 @@ import { import CheckedComponent from './CheckedComponent'; import {DashContextProvider} from './DashContext'; -// Define types for the input objects and the output differences -type Dictionary = Record; - -interface Difference { - obj1: any; - obj2: any; -} - -const findDifferences = ( - obj1: Dictionary, - obj2: Dictionary -): Record => { - const commonKeys = intersection(keys(obj1), keys(obj2)); - - const differingKeys = filter( - (key: string) => !equals(obj1[key], obj2[key]), - commonKeys - ) as string[]; - - const keysOnlyInObj2 = difference(keys(obj2), keys(obj1)); - - const differences: Record = reduce( - (acc: Record, key: string) => { - acc[key] = {obj1: obj1[key], obj2: obj2[key]}; - return acc; - }, - {}, - differingKeys as string[] - ); - - keysOnlyInObj2.forEach(key => { - differences[key] = {obj1: undefined, obj2: obj2[key]}; - }); - - return differences; -}; - type DashWrapperProps = { /** * Path of the component in the layout. @@ -113,7 +72,7 @@ function DashWrapper({ // Select both the component and it's props. // eslint-disable-next-line prefer-const - let [component, componentProps, h, changedProps] = useSelector( + let [component, componentProps, h, changedProps, renderType] = useSelector( selectDashProps(componentPath), selectDashPropsEqualityFn ); @@ -135,13 +94,6 @@ function DashWrapper({ }, [_newRender]); /* eslint-enable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */ - let propDifferences: any = {}; - if (memoizedProps.current) { - propDifferences = findDifferences( - memoizedProps.current, - componentProps - ); - } memoizedProps.current = componentProps; const setProps = (newProps: UpdatePropsPayload) => { @@ -198,7 +150,8 @@ function DashWrapper({ props: changedProps, itempath: componentPath, component, - config + config, + renderType: 'internal' }) ); }); @@ -261,6 +214,8 @@ function DashWrapper({ const extraProps = { setProps, + rendertype: newRender.current ? 'parent' : ( + changedProps ? renderType : 'parent'), ...extras }; @@ -277,15 +232,11 @@ function DashWrapper({ const childrenProp: string = childrenProps[i]; let childNewRender = 0; if ( - childrenProp - .split('.')[0] - .replace('[]', '') - .replace('{}', '') in propDifferences || childrenProp .split('.')[0] .replace('[]', '') .replace('{}', '') in changedProps || - newRender.current + newRender.current || !h ) { childNewRender = Date.now(); } @@ -498,7 +449,7 @@ function DashWrapper({ hydratedChildren = wrapChildrenProp( componentProps.children, ['children'], - 'children' in propDifferences || + !h || newRender.current || 'children' in changedProps ? Date.now() diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index 5db8c8d194..ccf75d7cb7 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, object]; +type SelectDashProps = [DashComponent, BaseDashProps, number, object, string]; export const selectDashProps = (componentPath: DashLayoutPath) => @@ -15,11 +15,13 @@ export const selectDashProps = const hash = state.layoutHashes[strPath]; let h = 0; let changedProps: object = {}; + let renderType: string = ''; if (hash) { - h = hash[0]; - changedProps = hash[1]; + h = hash['hash']; + changedProps = hash['changedProps']; + renderType = hash['renderType'] } - return [c, c?.props, h, changedProps]; + return [c, c?.props, h, changedProps, renderType]; }; export function selectDashPropsEqualityFn( From 889b4a5ca6cb912211e12a7a39da5e1716a30a11 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 31 Mar 2025 05:47:51 -0400 Subject: [PATCH 20/33] removed the new prop being passed down: `rendertype` --- dash/dash-renderer/src/reducers/reducer.js | 11 +++++++++-- dash/dash-renderer/src/wrapper/DashWrapper.tsx | 11 ++++------- dash/dash-renderer/src/wrapper/selectors.ts | 4 ++-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index 0cafbd4be8..10dc7d936d 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -31,8 +31,15 @@ function adjustHashes(state, action) { 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); + state = assoc( + strPath, + { + hash: prev + 1, + changedProps: action.payload.props, + renderType: action.payload.renderType + }, + state + ); return state; } diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 04846b880a..89758eedfd 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -72,7 +72,7 @@ function DashWrapper({ // Select both the component and it's props. // eslint-disable-next-line prefer-const - let [component, componentProps, h, changedProps, renderType] = useSelector( + let [component, componentProps, h, changedProps] = useSelector( selectDashProps(componentPath), selectDashPropsEqualityFn ); @@ -214,8 +214,6 @@ function DashWrapper({ const extraProps = { setProps, - rendertype: newRender.current ? 'parent' : ( - changedProps ? renderType : 'parent'), ...extras }; @@ -236,7 +234,8 @@ function DashWrapper({ .split('.')[0] .replace('[]', '') .replace('{}', '') in changedProps || - newRender.current || !h + newRender.current || + !h ) { childNewRender = Date.now(); } @@ -449,9 +448,7 @@ function DashWrapper({ hydratedChildren = wrapChildrenProp( componentProps.children, ['children'], - !h || - newRender.current || - 'children' in changedProps + !h || newRender.current || 'children' in changedProps ? Date.now() : 0 ); diff --git a/dash/dash-renderer/src/wrapper/selectors.ts b/dash/dash-renderer/src/wrapper/selectors.ts index ccf75d7cb7..46930bc47e 100644 --- a/dash/dash-renderer/src/wrapper/selectors.ts +++ b/dash/dash-renderer/src/wrapper/selectors.ts @@ -15,11 +15,11 @@ export const selectDashProps = const hash = state.layoutHashes[strPath]; let h = 0; let changedProps: object = {}; - let renderType: string = ''; + let renderType = ''; if (hash) { h = hash['hash']; changedProps = hash['changedProps']; - renderType = hash['renderType'] + renderType = hash['renderType']; } return [c, c?.props, h, changedProps, renderType]; }; From 9c8a62ad7aaee210af09f8ef3e1ca12c3eeb3b65 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 31 Mar 2025 06:34:34 -0400 Subject: [PATCH 21/33] adding `renderType` but making it a conditional prop that the component can subscribe to based upon its prop definitions --- .../dash-renderer/src/wrapper/DashWrapper.tsx | 19 ++++++++++++++++--- dash/dash-renderer/src/wrapper/wrapping.ts | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 89758eedfd..960d3f3dc6 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -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 { @@ -72,7 +77,7 @@ function DashWrapper({ // Select both the component and it's props. // eslint-disable-next-line prefer-const - let [component, componentProps, h, changedProps] = useSelector( + let [component, componentProps, h, changedProps, renderType] = useSelector( selectDashProps(componentPath), selectDashPropsEqualityFn ); @@ -215,7 +220,15 @@ function DashWrapper({ const extraProps = { setProps, ...extras - }; + } as {[key: string]: any}; + + if (checkRenderTypeProp(component)) { + extraProps['renderType'] = newRender.current + ? 'parent' + : changedProps + ? renderType + : 'parent'; + } const setHydratedProps = (component: any, componentProps: any) => { // Hydrate components props diff --git a/dash/dash-renderer/src/wrapper/wrapping.ts b/dash/dash-renderer/src/wrapper/wrapping.ts index baf0e4f6f3..88aedd90d1 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,18 @@ export function getComponentLayout( ): DashComponent { return path(componentPath, state.layout) as DashComponent; } + +export function checkRenderTypeProp(componentDefinition: any) { + return ( + 'renderType' in + pathOr( + {}, + [ + componentDefinition?.namespace, + componentDefinition?.type, + 'propTypes' + ], + window as any + ) + ); +} From ef41561c76b2442c62a0ec368ddefcd41512e56b Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 31 Mar 2025 09:44:06 -0400 Subject: [PATCH 22/33] adding test component and test for rendertype prop --- .../src/components/RenderType.js | 21 ++++++ @plotly/dash-test-components/src/index.js | 2 + .../integration/renderer/test_render_type.py | 67 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 @plotly/dash-test-components/src/components/RenderType.js create mode 100644 tests/integration/renderer/test_render_type.py 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..1b402f15d2 --- /dev/null +++ b/@plotly/dash-test-components/src/components/RenderType.js @@ -0,0 +1,21 @@ +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.renderType} + +
; +}; + +RenderType.propTypes = { + id: PropTypes.string, + renderType: PropTypes.string, + n_clicks: PropTypes.number, + setProps: PropTypes.func +}; +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/tests/integration/renderer/test_render_type.py b/tests/integration/renderer/test_render_type.py new file mode 100644 index 0000000000..93e0534455 --- /dev/null +++ b/tests/integration/renderer/test_render_type.py @@ -0,0 +1,67 @@ +import time +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") From c409895fa45439361d7ed55ac63b42b140f302c3 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 31 Mar 2025 10:03:03 -0400 Subject: [PATCH 23/33] removing unused import --- tests/integration/renderer/test_render_type.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/renderer/test_render_type.py b/tests/integration/renderer/test_render_type.py index 93e0534455..17a6cfbae3 100644 --- a/tests/integration/renderer/test_render_type.py +++ b/tests/integration/renderer/test_render_type.py @@ -1,4 +1,3 @@ -import time from dash import Dash, Input, Output, html import json From ce1da9ae321235fc56efed63238f016359410fce Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:28:55 -0400 Subject: [PATCH 24/33] adjusting `renderType` to `dashRenderType` if the dev wants to subscribe, they need to place on the component: `namespace.component.dashRenderType = true` --- @plotly/dash-test-components/src/components/RenderType.js | 6 ++++-- dash/dash-renderer/src/wrapper/DashWrapper.tsx | 2 +- dash/dash-renderer/src/wrapper/wrapping.ts | 3 +-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/@plotly/dash-test-components/src/components/RenderType.js b/@plotly/dash-test-components/src/components/RenderType.js index 1b402f15d2..aa99f1feb8 100644 --- a/@plotly/dash-test-components/src/components/RenderType.js +++ b/@plotly/dash-test-components/src/components/RenderType.js @@ -7,15 +7,17 @@ const RenderType = (props) => { } return
- {props.renderType} + {props.dashRenderType}
; }; RenderType.propTypes = { id: PropTypes.string, - renderType: PropTypes.string, + dashRenderType: PropTypes.string, n_clicks: PropTypes.number, setProps: PropTypes.func }; + +RenderType.dashRenderType = true; export default RenderType; diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 960d3f3dc6..467c4d79b0 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -223,7 +223,7 @@ function DashWrapper({ } as {[key: string]: any}; if (checkRenderTypeProp(component)) { - extraProps['renderType'] = newRender.current + extraProps['dashRenderType'] = newRender.current ? 'parent' : changedProps ? renderType diff --git a/dash/dash-renderer/src/wrapper/wrapping.ts b/dash/dash-renderer/src/wrapper/wrapping.ts index 88aedd90d1..9444d6191c 100644 --- a/dash/dash-renderer/src/wrapper/wrapping.ts +++ b/dash/dash-renderer/src/wrapper/wrapping.ts @@ -64,13 +64,12 @@ export function getComponentLayout( export function checkRenderTypeProp(componentDefinition: any) { return ( - 'renderType' in + 'dashRenderType' in pathOr( {}, [ componentDefinition?.namespace, componentDefinition?.type, - 'propTypes' ], window as any ) From f0db8c5cbc60216b12e7d8328881b9a52167272d Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:36:15 -0400 Subject: [PATCH 25/33] replacing `Date.now()` with new object to force rerender --- dash/dash-renderer/src/wrapper/DashWrapper.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 467c4d79b0..54df206090 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -241,7 +241,7 @@ function DashWrapper({ for (let i = 0; i < childrenProps.length; i++) { const childrenProp: string = childrenProps[i]; - let childNewRender = 0; + let childNewRender: any = 0; if ( childrenProp .split('.')[0] @@ -250,7 +250,7 @@ function DashWrapper({ newRender.current || !h ) { - childNewRender = Date.now(); + childNewRender = {}; } const handleObject = (obj: any, opath: DashLayoutPath) => { return mapObjIndexed( @@ -462,7 +462,7 @@ function DashWrapper({ componentProps.children, ['children'], !h || newRender.current || 'children' in changedProps - ? Date.now() + ? {} : 0 ); } From 6588aac16149d1c7fb1bef35a25a5aac42ebc0b2 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:44:55 -0400 Subject: [PATCH 26/33] adjusting for lint --- dash/dash-renderer/src/wrapper/wrapping.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/dash-renderer/src/wrapper/wrapping.ts b/dash/dash-renderer/src/wrapper/wrapping.ts index 9444d6191c..0eb5ac4f6e 100644 --- a/dash/dash-renderer/src/wrapper/wrapping.ts +++ b/dash/dash-renderer/src/wrapper/wrapping.ts @@ -69,7 +69,7 @@ export function checkRenderTypeProp(componentDefinition: any) { {}, [ componentDefinition?.namespace, - componentDefinition?.type, + componentDefinition?.type ], window as any ) From 5e1bdf31b7f07a258d5c91a95394d999094468b6 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:00:36 -0400 Subject: [PATCH 27/33] adjustments from feedback --- dash/dash-renderer/src/actions/callbacks.ts | 4 --- .../src/observers/executedCallbacks.ts | 6 +--- dash/dash-renderer/src/reducers/reducer.js | 29 ++++++++----------- .../src/utils/clientsideFunctions.ts | 6 +--- .../dash-renderer/src/wrapper/DashWrapper.tsx | 16 ++++------ dash/dash-renderer/src/wrapper/wrapping.ts | 5 +--- 6 files changed, 20 insertions(+), 46 deletions(-) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index 917e942ab0..8164063f4c 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -34,7 +34,6 @@ import { CallbackResponseData, SideUpdateOutput } from '../types/callbacks'; -import {getComponentLayout} from '../wrapper/wrapping'; import {isMultiValued, stringifyId, isMultiOutputProp} from './dependencies'; import {urlBase} from './utils'; import {getCSRFHeader, dispatchError} from '.'; @@ -359,13 +358,10 @@ function updateComponent(component_id: any, props: any, cb: ICallbackPayload) { // error. return; } - const component = getComponentLayout(componentPath, _state); dispatch( updateProps({ props, itempath: componentPath, - component, - config, renderType: 'callback' }) ); diff --git a/dash/dash-renderer/src/observers/executedCallbacks.ts b/dash/dash-renderer/src/observers/executedCallbacks.ts index 3da7dda3d9..a090d9b7c3 100644 --- a/dash/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash/dash-renderer/src/observers/executedCallbacks.ts @@ -34,7 +34,6 @@ import {ICallback, IStoredCallback} from '../types/callbacks'; import {updateProps, setPaths, handleAsyncError} from '../actions'; import {getPath, computePaths} from '../actions/paths'; -import {getComponentLayout} from '../wrapper/wrapping'; import {applyPersistence, prunePersistence} from '../persistence'; import {IStoreObserverDefinition} from '../StoreObserver'; @@ -47,7 +46,7 @@ const observer: IStoreObserverDefinition = { function applyProps(id: any, updatedProps: any) { const _state = getState(); - const {layout, paths, config} = _state; + const {layout, paths} = _state; const itempath = getPath(paths, id); if (!itempath) { return false; @@ -65,14 +64,11 @@ 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); - const component = getComponentLayout(itempath, _state); dispatch( updateProps({ itempath, props, source: 'response', - component, - config, renderType: 'callback' }) ); diff --git a/dash/dash-renderer/src/reducers/reducer.js b/dash/dash-renderer/src/reducers/reducer.js index 10dc7d936d..56d2c82ad2 100644 --- a/dash/dash-renderer/src/reducers/reducer.js +++ b/dash/dash-renderer/src/reducers/reducer.js @@ -27,22 +27,6 @@ export const apiRequests = [ 'loginRequest' ]; -function adjustHashes(state, action) { - 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; -} - const layoutHashes = (state = {}, action) => { if ( includes(action.type, [ @@ -53,7 +37,18 @@ const layoutHashes = (state = {}, action) => { ) { // Let us compare the paths sums to get updates without triggering // render on the parent containers. - return adjustHashes(state, action); + 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; }; diff --git a/dash/dash-renderer/src/utils/clientsideFunctions.ts b/dash/dash-renderer/src/utils/clientsideFunctions.ts index 4c4d4f1428..fc22877d70 100644 --- a/dash/dash-renderer/src/utils/clientsideFunctions.ts +++ b/dash/dash-renderer/src/utils/clientsideFunctions.ts @@ -1,7 +1,6 @@ import {updateProps, notifyObservers} from '../actions/index'; import {getPath} from '../actions/paths'; import {getStores} from './stores'; -import {getComponentLayout} from '../wrapper/wrapping'; /** * Set the props of a dash component by id or path. @@ -18,19 +17,16 @@ function set_props( const {dispatch, getState} = ds[y]; let componentPath; const _state = getState(); - const {paths, config} = _state; + const {paths} = _state; if (!Array.isArray(idOrPath)) { componentPath = getPath(paths, idOrPath); } else { componentPath = idOrPath; } - const component = getComponentLayout(componentPath, _state); dispatch( updateProps({ props, itempath: componentPath, - component, - config, renderType: 'clientsideApi' }) ); diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 54df206090..ae127cc049 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -63,8 +63,8 @@ type MemoizedPropsType = { function DashWrapper({ componentPath, _dashprivate_error, - _passedComponent, - _newRender, + _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(); @@ -84,7 +84,7 @@ function DashWrapper({ /* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */ // @ts-ignore - const newlyRendered = useMemo(() => { + const newlyRendered: any = useMemo(() => { if (_newRender) { memoizedProps.current = {}; newRender.current = true; @@ -154,8 +154,6 @@ function DashWrapper({ updateProps({ props: changedProps, itempath: componentPath, - component, - config, renderType: 'internal' }) ); @@ -461,14 +459,12 @@ function DashWrapper({ hydratedChildren = wrapChildrenProp( componentProps.children, ['children'], - !h || newRender.current || 'children' in changedProps - ? {} - : 0 + !h || newRender.current || 'children' in changedProps ? {} : 0 ); } newRender.current = false; - const rendered = config.props_check ? ( + return config.props_check ? ( Date: Tue, 1 Apr 2025 10:14:21 -0400 Subject: [PATCH 28/33] adjusting for `state` no longer needed in dispatch --- dash/dash-renderer/src/actions/callbacks.ts | 3 +-- dash/dash-renderer/src/observers/executedCallbacks.ts | 3 +-- dash/dash-renderer/src/utils/clientsideFunctions.ts | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index 8164063f4c..9765c8bba1 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -336,8 +336,7 @@ async function handleClientside( function updateComponent(component_id: any, props: any, cb: ICallbackPayload) { return function (dispatch: any, getState: any) { - const _state = getState(); - const {paths, config} = _state; + const {paths, config} = getState(); const componentPath = getPath(paths, component_id); if (!componentPath) { if (!config.suppress_callback_exceptions) { diff --git a/dash/dash-renderer/src/observers/executedCallbacks.ts b/dash/dash-renderer/src/observers/executedCallbacks.ts index a090d9b7c3..f82a24bdf0 100644 --- a/dash/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash/dash-renderer/src/observers/executedCallbacks.ts @@ -45,8 +45,7 @@ const observer: IStoreObserverDefinition = { } = getState(); function applyProps(id: any, updatedProps: any) { - const _state = getState(); - const {layout, paths} = _state; + const {layout, paths} = getState(); const itempath = getPath(paths, id); if (!itempath) { return false; diff --git a/dash/dash-renderer/src/utils/clientsideFunctions.ts b/dash/dash-renderer/src/utils/clientsideFunctions.ts index fc22877d70..4859df41bf 100644 --- a/dash/dash-renderer/src/utils/clientsideFunctions.ts +++ b/dash/dash-renderer/src/utils/clientsideFunctions.ts @@ -16,8 +16,7 @@ function set_props( for (let y = 0; y < ds.length; y++) { const {dispatch, getState} = ds[y]; let componentPath; - const _state = getState(); - const {paths} = _state; + const {paths} = getState(); if (!Array.isArray(idOrPath)) { componentPath = getPath(paths, idOrPath); } else { From 3ade208a8ad416b81da623927c0c298161e7eb39 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Tue, 1 Apr 2025 10:53:29 -0400 Subject: [PATCH 29/33] adjustments based on feedback --- .../dash-renderer/src/wrapper/DashWrapper.tsx | 63 ++++++++++--------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index ae127cc049..16af483b6d 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -71,33 +71,33 @@ function DashWrapper({ const memoizedKeys: MutableRefObject = useRef({}); const memoizedProps: 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. - // eslint-disable-next-line prefer-const - let [component, componentProps, h, changedProps, renderType] = 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; - /* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */ - // @ts-ignore - const newlyRendered: any = useMemo(() => { + useMemo(() => { if (_newRender) { memoizedProps.current = {}; newRender.current = true; - h = 0; + renderH = 0; if (h in memoizedKeys.current) { delete memoizedKeys.current[h]; } } else { newRender.current = false; } - return newRender.current; }, [_newRender]); - /* eslint-enable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */ memoizedProps.current = componentProps; @@ -163,8 +163,8 @@ function DashWrapper({ const createContainer = useCallback( (container, containerPath, _childNewRender, key = undefined) => { - if (isSimpleComponent(component)) { - return component; + if (isSimpleComponent(container)) { + return container; } return ( { if (newRender.current) { - component = _passedComponent; - componentProps = _passedComponent?.props; + renderComponent = _passedComponent; + renderComponentProps = _passedComponent?.props; } - if (!component) { + if (!renderComponent) { return null; } - const element = Registry.resolve(component); - const hydratedProps = setHydratedProps(component, componentProps); + const element = Registry.resolve(renderComponent); + const hydratedProps = setHydratedProps( + renderComponent, + renderComponentProps + ); let hydratedChildren: any; - if (componentProps.children !== undefined) { + if (renderComponentProps.children !== undefined) { hydratedChildren = wrapChildrenProp( - componentProps.children, + renderComponentProps.children, ['children'], - !h || newRender.current || 'children' in changedProps ? {} : 0 + !renderH || newRender.current || 'children' in changedProps + ? {} + : 0 ); } newRender.current = false; @@ -468,7 +473,7 @@ function DashWrapper({ {createElement( element, @@ -483,14 +488,14 @@ function DashWrapper({ }; let hydrated = null; - if (h in memoizedKeys.current && !newRender.current) { - hydrated = React.isValidElement(memoizedKeys.current[h]) - ? memoizedKeys.current[h] + if (renderH in memoizedKeys.current && !newRender.current) { + hydrated = React.isValidElement(memoizedKeys.current[renderH]) + ? memoizedKeys.current[renderH] : null; } if (!hydrated) { hydrated = hydrateFunc(); - memoizedKeys.current = {[h]: hydrated}; + memoizedKeys.current = {[renderH]: hydrated}; } return component ? ( From 6f744f0d1b842137247d67d8c75038461a09a674 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Tue, 1 Apr 2025 11:15:20 -0400 Subject: [PATCH 30/33] reverting errant adjustment --- dash/dash-renderer/src/wrapper/DashWrapper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 16af483b6d..62bdece5cc 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -163,8 +163,8 @@ function DashWrapper({ const createContainer = useCallback( (container, containerPath, _childNewRender, key = undefined) => { - if (isSimpleComponent(container)) { - return container; + if (isSimpleComponent(renderComponent)) { + return renderComponent; } return ( Date: Tue, 1 Apr 2025 11:16:42 -0400 Subject: [PATCH 31/33] additional `component` -> `renderComponent` adjustments --- dash/dash-renderer/src/wrapper/DashWrapper.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 62bdece5cc..5a87ed7a24 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -498,13 +498,13 @@ function DashWrapper({ memoizedKeys.current = {[renderH]: hydrated}; } - return component ? ( + return renderComponent ? ( Date: Tue, 1 Apr 2025 12:13:00 -0400 Subject: [PATCH 32/33] adjusting for missing variable swaps and removing unused `memoizeProps` --- dash/dash-renderer/src/wrapper/DashWrapper.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 5a87ed7a24..82bee5b0ba 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -55,11 +55,6 @@ type MemoizedKeysType = { [key: string]: React.ReactNode | null; // This includes React elements, strings, numbers, etc. }; -// Define a type for the memoized props -type MemoizedPropsType = { - [key: string]: any; -}; - function DashWrapper({ componentPath, _dashprivate_error, @@ -69,7 +64,6 @@ function DashWrapper({ }: DashWrapperProps) { const dispatch = useDispatch(); const memoizedKeys: MutableRefObject = useRef({}); - const memoizedProps: MutableRefObject = useRef({}); const newRender = useRef(false); let renderComponent: any = null; let renderComponentProps: any = null; @@ -88,21 +82,18 @@ function DashWrapper({ useMemo(() => { if (_newRender) { - memoizedProps.current = {}; newRender.current = true; renderH = 0; - if (h in memoizedKeys.current) { - delete memoizedKeys.current[h]; + if (renderH in memoizedKeys.current) { + delete memoizedKeys.current[renderH]; } } else { newRender.current = false; } }, [_newRender]); - memoizedProps.current = componentProps; - const setProps = (newProps: UpdatePropsPayload) => { - const {id} = componentProps; + const {id} = renderComponentProps; const {_dash_error, ...restProps} = newProps; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -137,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) { From cde9acf82dbc0834e937109d9bbb1bacd776cde5 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Tue, 1 Apr 2025 12:28:17 -0400 Subject: [PATCH 33/33] updating change log entry --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5b3b2c6db..6681cbaef6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +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] changes to the rendering logic +- [#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