From f8928a18046b3225d982e47c3326b69a7498b27e Mon Sep 17 00:00:00 2001 From: petar-qb Date: Wed, 29 Jan 2025 15:13:11 +0100 Subject: [PATCH 1/6] Add enable_persistence that when True enables storing the callback outputs in the persistence storage --- dash/_callback.py | 12 +++ dash/dash-renderer/src/TreeContainer.js | 8 +- dash/dash-renderer/src/actions/index.js | 18 +++- .../src/observers/executedCallbacks.ts | 30 +++++-- dash/dash-renderer/src/persistence.js | 82 ++++++++++++------- dash/dash-renderer/src/types/callbacks.ts | 1 + 6 files changed, 104 insertions(+), 47 deletions(-) diff --git a/dash/_callback.py b/dash/_callback.py index 071c209dec..6e4b347b46 100644 --- a/dash/_callback.py +++ b/dash/_callback.py @@ -71,6 +71,7 @@ def callback( manager=None, cache_args_to_ignore=None, on_error: Optional[Callable[[Exception], Any]] = None, + enable_persistence: bool = False, **_kwargs, ): """ @@ -145,6 +146,10 @@ def callback( Function to call when the callback raises an exception. Receives the exception object as first argument. The callback_context can be used to access the original callback inputs, states and output. + :param enable_persistence: + Mark to enable persistence change from the callback. If it's True, + this will enable storing the callback outputs in the browser's + persistence storage. """ long_spec = None @@ -195,6 +200,7 @@ def callback( manager=manager, running=running, on_error=on_error, + enable_persistence=enable_persistence, ) @@ -237,6 +243,7 @@ def insert_callback( running=None, dynamic_creator: Optional[bool] = False, no_output=False, + enable_persistence: bool = False, ): if prevent_initial_call is None: prevent_initial_call = config_prevent_initial_callbacks @@ -260,6 +267,7 @@ def insert_callback( }, "dynamic_creator": dynamic_creator, "no_output": no_output, + "enable_persistence": enable_persistence, } if running: callback_spec["running"] = running @@ -315,6 +323,7 @@ def register_callback( manager = _kwargs.get("manager") running = _kwargs.get("running") on_error = _kwargs.get("on_error") + enable_persistence = _kwargs.get("enable_persistence") if running is not None: if not isinstance(running[0], (list, tuple)): running = [running] @@ -340,6 +349,7 @@ def register_callback( dynamic_creator=allow_dynamic_callbacks, running=running, no_output=not has_output, + enable_persistence=enable_persistence, ) # pylint: disable=too-many-locals @@ -602,6 +612,7 @@ def register_clientside_callback( ): output, inputs, state, prevent_initial_call = handle_callback_args(args, kwargs) no_output = isinstance(output, (list,)) and len(output) == 0 + enable_persistence = kwargs.get("enable_persistence") insert_callback( callback_list, callback_map, @@ -613,6 +624,7 @@ def register_clientside_callback( None, prevent_initial_call, no_output=no_output, + enable_persistence=enable_persistence, ) # If JS source is explicitly given, create a namespace and function diff --git a/dash/dash-renderer/src/TreeContainer.js b/dash/dash-renderer/src/TreeContainer.js index 25b51bc493..3d08aaa24a 100644 --- a/dash/dash-renderer/src/TreeContainer.js +++ b/dash/dash-renderer/src/TreeContainer.js @@ -26,7 +26,6 @@ import { } from 'ramda'; import {notifyObservers, updateProps, onError} from './actions'; import isSimpleComponent from './isSimpleComponent'; -import {recordUiEdit} from './persistence'; import ComponentErrorBoundary from './components/error/ComponentErrorBoundary.react'; import checkPropTypes from './checkPropTypes'; import {getWatchedKeys, stringifyId} from './actions/dependencies'; @@ -132,8 +131,7 @@ class BaseTreeContainer extends Component { } setProps(newProps) { - const {_dashprivate_dispatch, _dashprivate_path, _dashprivate_layout} = - this.props; + const {_dashprivate_dispatch, _dashprivate_path} = this.props; const oldProps = this.getLayoutProps(); const {id} = oldProps; @@ -163,10 +161,6 @@ class BaseTreeContainer extends Component { ); batch(() => { - // setProps here is triggered by the UI - record these changes - // for persistence - recordUiEdit(_dashprivate_layout, newProps, dispatch); - // Only dispatch changes to Dash if a watched prop changed if (watchedKeys.length) { dispatch( diff --git a/dash/dash-renderer/src/actions/index.js b/dash/dash-renderer/src/actions/index.js index 2b1dd51324..5ce68888ca 100644 --- a/dash/dash-renderer/src/actions/index.js +++ b/dash/dash-renderer/src/actions/index.js @@ -1,4 +1,4 @@ -import {once} from 'ramda'; +import {once, path} from 'ramda'; import {createAction} from 'redux-actions'; import {addRequestedCallbacks} from './callbacks'; import {getAppState} from '../reducers/constants'; @@ -7,6 +7,7 @@ import cookie from 'cookie'; import {validateCallbacksToLayout} from './dependencies'; import {includeObservers, getLayoutCallbacks} from './dependencies_ts'; import {getPath} from './paths'; +import {recordUiEdit} from '../persistence'; export const onError = createAction(getAction('ON_ERROR')); export const setAppLifecycle = createAction(getAction('SET_APP_LIFECYCLE')); @@ -17,7 +18,20 @@ export const setHooks = createAction(getAction('SET_HOOKS')); export const setLayout = createAction(getAction('SET_LAYOUT')); export const setPaths = createAction(getAction('SET_PATHS')); export const setRequestQueue = createAction(getAction('SET_REQUEST_QUEUE')); -export const updateProps = createAction(getAction('ON_PROP_CHANGE')); + +// Change the variable name of the action +export const onPropChange = createAction(getAction('ON_PROP_CHANGE')); + +export function updateProps(payload) { + return (dispatch, getState) => { + const {enable_persistence} = payload; + if (payload.source !== 'response' || enable_persistence) { + const component = path(payload.itempath, getState().layout); + recordUiEdit(component, payload.props, dispatch); + } + dispatch(onPropChange(payload)); + }; +} export const dispatchError = dispatch => (message, lines) => dispatch( diff --git a/dash/dash-renderer/src/observers/executedCallbacks.ts b/dash/dash-renderer/src/observers/executedCallbacks.ts index 1249252704..5db690bf8d 100644 --- a/dash/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash/dash-renderer/src/observers/executedCallbacks.ts @@ -44,31 +44,41 @@ const observer: IStoreObserverDefinition = { callbacks: {executed} } = getState(); - function applyProps(id: any, updatedProps: any) { + function applyProps( + id: any, + updatedProps: any, + enable_persistence: boolean + ) { const {layout, paths} = getState(); const itempath = getPath(paths, id); if (!itempath) { return false; } - // This is a callback-generated update. // Check if this invalidates existing persisted prop values, // or if persistence changed, whether this updates other props. updatedProps = prunePersistence( path(itempath, layout), updatedProps, - dispatch + dispatch, + enable_persistence ); - // 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); + // This is a callback-generated update. + // Apply persisted values from the persistence storage to the UI values only if "enable_persistence=False". + const {props} = applyPersistence( + {props: updatedProps}, + dispatch, + enable_persistence + ); + // Update properties and set value within the persistence storage only if "enable_persistence=True". dispatch( updateProps({ itempath, props, - source: 'response' + source: 'response', + enable_persistence }) ); @@ -103,7 +113,11 @@ const observer: IStoreObserverDefinition = { } = getState(); // Components will trigger callbacks on their own as required (eg. derived) - const appliedProps = applyProps(parsedId, props); + const appliedProps = applyProps( + parsedId, + props, + cb.callback.enable_persistence + ); // Add callbacks for modified inputs requestedCallbacks = concat( diff --git a/dash/dash-renderer/src/persistence.js b/dash/dash-renderer/src/persistence.js index b7bc2719ba..6cdbd2fe88 100644 --- a/dash/dash-renderer/src/persistence.js +++ b/dash/dash-renderer/src/persistence.js @@ -307,6 +307,10 @@ const getProps = layout => { }; export function recordUiEdit(layout, newProps, dispatch) { + if (newProps === undefined) { + return; + } + const { canPersist, id, @@ -316,43 +320,52 @@ export function recordUiEdit(layout, newProps, dispatch) { persisted_props, persistence_type } = getProps(layout); - if (!canPersist || !persistence) { - return; + + if (canPersist && persistence) { + forEach(persistedProp => { + const [propName, propPart] = persistedProp.split('.'); + if (newProps[propName] !== undefined) { + const storage = getStore(persistence_type, dispatch); + const {extract} = getTransform(element, propName, propPart); + const valsKey = getValsKey(id, persistedProp, persistence); + + let originalVal = storage.hasItem(valsKey) + ? storage.getItem(valsKey)[1] + : extract(props[propName]); + let newVal = extract(newProps[propName]); + + storage.setItem(valsKey, [newVal, originalVal], dispatch); + } + }, persisted_props); } - forEach(persistedProp => { - const [propName, propPart] = persistedProp.split('.'); - if (newProps[propName] !== undefined) { - const storage = getStore(persistence_type, dispatch); - const {extract} = getTransform(element, propName, propPart); - - const valsKey = getValsKey(id, persistedProp, persistence); - let originalVal = extract(props[propName]); - const newVal = extract(newProps[propName]); - - // mainly for nested props with multiple persisted parts, it's - // possible to have the same value as before - should not store - // in this case. - if (originalVal !== newVal) { - if (storage.hasItem(valsKey)) { - originalVal = storage.getItem(valsKey)[1]; - } - const vals = - originalVal === undefined - ? [newVal] - : [newVal, originalVal]; - storage.setItem(valsKey, vals, dispatch); + // Recursively record UI edits for children + const {children} = props; + if (Array.isArray(children)) { + children.forEach((child, i) => { + if ( + type(child) === 'Object' && + child.props && + newProps['children'] !== undefined + ) { + recordUiEdit(child, newProps['children'][i]['props'], dispatch); } - } - }, persisted_props); + }); + } else if ( + type(children) === 'Object' && + children.props && + newProps['children'] !== undefined + ) { + recordUiEdit(children, newProps['children']['props'], dispatch); + } } /* * Used for entire layouts (on load) or partial layouts (from children * callbacks) to apply previously-stored UI edits to components */ -export function applyPersistence(layout, dispatch) { - if (type(layout) !== 'Object' || !layout.props) { +export function applyPersistence(layout, dispatch, enable_persistence) { + if (type(layout) !== 'Object' || !layout.props || enable_persistence) { return layout; } @@ -447,7 +460,12 @@ function persistenceMods(layout, component, path, dispatch) { * these override UI-driven edits of those exact props * but not for props nested inside children */ -export function prunePersistence(layout, newProps, dispatch) { +export function prunePersistence( + layout, + newProps, + dispatch, + enable_persistence +) { const { canPersist, id, @@ -462,7 +480,11 @@ export function prunePersistence(layout, newProps, dispatch) { propName in newProps ? newProps[propName] : prevVal; const finalPersistence = getFinal('persistence', persistence); - if (!canPersist || !(persistence || finalPersistence)) { + if ( + !canPersist || + !(persistence || finalPersistence) || + enable_persistence + ) { return newProps; } diff --git a/dash/dash-renderer/src/types/callbacks.ts b/dash/dash-renderer/src/types/callbacks.ts index d1cf636776..0a48cb4b51 100644 --- a/dash/dash-renderer/src/types/callbacks.ts +++ b/dash/dash-renderer/src/types/callbacks.ts @@ -15,6 +15,7 @@ export interface ICallbackDefinition { dynamic_creator?: boolean; running: any; no_output?: boolean; + enable_persistence?: boolean; } export interface ICallbackProperty { From 5802884b53279f5b7ebe29a5f2233fd5e4478494 Mon Sep 17 00:00:00 2001 From: petar-qb Date: Thu, 30 Jan 2025 13:15:22 +0100 Subject: [PATCH 2/6] Minor changes --- dash/_callback.py | 6 +++--- dash/dash-renderer/src/observers/executedCallbacks.ts | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/dash/_callback.py b/dash/_callback.py index 6e4b347b46..8ab0f8257a 100644 --- a/dash/_callback.py +++ b/dash/_callback.py @@ -147,9 +147,9 @@ def callback( exception object as first argument. The callback_context can be used to access the original callback inputs, states and output. :param enable_persistence: - Mark to enable persistence change from the callback. If it's True, - this will enable storing the callback outputs in the browser's - persistence storage. + Indicates whether the callback can write in the persistence storage. + If set to `True`, any outputs with persistence enabled will have their values + stored in the browser's persistence storage when updated by the callback. """ long_spec = None diff --git a/dash/dash-renderer/src/observers/executedCallbacks.ts b/dash/dash-renderer/src/observers/executedCallbacks.ts index 5db690bf8d..16cc82b7b4 100644 --- a/dash/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash/dash-renderer/src/observers/executedCallbacks.ts @@ -55,6 +55,7 @@ const observer: IStoreObserverDefinition = { return false; } + // This is a callback-generated update. // Check if this invalidates existing persisted prop values, // or if persistence changed, whether this updates other props. updatedProps = prunePersistence( @@ -64,15 +65,12 @@ const observer: IStoreObserverDefinition = { enable_persistence ); - // This is a callback-generated update. - // Apply persisted values from the persistence storage to the UI values only if "enable_persistence=False". const {props} = applyPersistence( {props: updatedProps}, dispatch, enable_persistence ); - // Update properties and set value within the persistence storage only if "enable_persistence=True". dispatch( updateProps({ itempath, From 3e8a9341a308e5dbe3ca56d4fb16cef77a5d6c78 Mon Sep 17 00:00:00 2001 From: petar-qb Date: Mon, 3 Feb 2025 15:31:57 +0100 Subject: [PATCH 3/6] Fix npm run build:js errors --- dash/dash-renderer/src/observers/executedCallbacks.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dash/dash-renderer/src/observers/executedCallbacks.ts b/dash/dash-renderer/src/observers/executedCallbacks.ts index 16cc82b7b4..681ef5df43 100644 --- a/dash/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash/dash-renderer/src/observers/executedCallbacks.ts @@ -12,6 +12,8 @@ import { } from 'ramda'; import {IStoreState} from '../store'; +import {ThunkDispatch} from 'redux-thunk'; +import {AnyAction} from 'redux'; import { aggregateCallbacks, @@ -71,7 +73,7 @@ const observer: IStoreObserverDefinition = { enable_persistence ); - dispatch( + (dispatch as ThunkDispatch)( updateProps({ itempath, props, @@ -110,11 +112,16 @@ const observer: IStoreObserverDefinition = { paths: oldPaths } = getState(); + const enable_persistence = + cb.callback.enable_persistence === undefined + ? false + : cb.callback.enable_persistence; + // Components will trigger callbacks on their own as required (eg. derived) const appliedProps = applyProps( parsedId, props, - cb.callback.enable_persistence + enable_persistence ); // Add callbacks for modified inputs From 1a3173ed889187f72e4aad725c65e7198f8a8ec4 Mon Sep 17 00:00:00 2001 From: petar-qb Date: Fri, 14 Feb 2025 16:01:23 +0100 Subject: [PATCH 4/6] Solution without the enable_persistence flag --- dash/_callback.py | 12 ----- dash/dash-renderer/src/actions/index.js | 7 +-- .../src/observers/executedCallbacks.ts | 52 +++++++------------ dash/dash-renderer/src/persistence.js | 17 ++---- dash/dash-renderer/src/types/callbacks.ts | 1 - 5 files changed, 24 insertions(+), 65 deletions(-) diff --git a/dash/_callback.py b/dash/_callback.py index 8ab0f8257a..071c209dec 100644 --- a/dash/_callback.py +++ b/dash/_callback.py @@ -71,7 +71,6 @@ def callback( manager=None, cache_args_to_ignore=None, on_error: Optional[Callable[[Exception], Any]] = None, - enable_persistence: bool = False, **_kwargs, ): """ @@ -146,10 +145,6 @@ def callback( Function to call when the callback raises an exception. Receives the exception object as first argument. The callback_context can be used to access the original callback inputs, states and output. - :param enable_persistence: - Indicates whether the callback can write in the persistence storage. - If set to `True`, any outputs with persistence enabled will have their values - stored in the browser's persistence storage when updated by the callback. """ long_spec = None @@ -200,7 +195,6 @@ def callback( manager=manager, running=running, on_error=on_error, - enable_persistence=enable_persistence, ) @@ -243,7 +237,6 @@ def insert_callback( running=None, dynamic_creator: Optional[bool] = False, no_output=False, - enable_persistence: bool = False, ): if prevent_initial_call is None: prevent_initial_call = config_prevent_initial_callbacks @@ -267,7 +260,6 @@ def insert_callback( }, "dynamic_creator": dynamic_creator, "no_output": no_output, - "enable_persistence": enable_persistence, } if running: callback_spec["running"] = running @@ -323,7 +315,6 @@ def register_callback( manager = _kwargs.get("manager") running = _kwargs.get("running") on_error = _kwargs.get("on_error") - enable_persistence = _kwargs.get("enable_persistence") if running is not None: if not isinstance(running[0], (list, tuple)): running = [running] @@ -349,7 +340,6 @@ def register_callback( dynamic_creator=allow_dynamic_callbacks, running=running, no_output=not has_output, - enable_persistence=enable_persistence, ) # pylint: disable=too-many-locals @@ -612,7 +602,6 @@ def register_clientside_callback( ): output, inputs, state, prevent_initial_call = handle_callback_args(args, kwargs) no_output = isinstance(output, (list,)) and len(output) == 0 - enable_persistence = kwargs.get("enable_persistence") insert_callback( callback_list, callback_map, @@ -624,7 +613,6 @@ def register_clientside_callback( None, prevent_initial_call, no_output=no_output, - enable_persistence=enable_persistence, ) # If JS source is explicitly given, create a namespace and function diff --git a/dash/dash-renderer/src/actions/index.js b/dash/dash-renderer/src/actions/index.js index 5ce68888ca..0f0db0d6b2 100644 --- a/dash/dash-renderer/src/actions/index.js +++ b/dash/dash-renderer/src/actions/index.js @@ -24,11 +24,8 @@ export const onPropChange = createAction(getAction('ON_PROP_CHANGE')); export function updateProps(payload) { return (dispatch, getState) => { - const {enable_persistence} = payload; - if (payload.source !== 'response' || enable_persistence) { - const component = path(payload.itempath, getState().layout); - recordUiEdit(component, payload.props, dispatch); - } + const component = path(payload.itempath, getState().layout); + recordUiEdit(component, payload.props, dispatch); dispatch(onPropChange(payload)); }; } diff --git a/dash/dash-renderer/src/observers/executedCallbacks.ts b/dash/dash-renderer/src/observers/executedCallbacks.ts index 681ef5df43..3c1d019f16 100644 --- a/dash/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash/dash-renderer/src/observers/executedCallbacks.ts @@ -12,8 +12,6 @@ import { } from 'ramda'; import {IStoreState} from '../store'; -import {ThunkDispatch} from 'redux-thunk'; -import {AnyAction} from 'redux'; import { aggregateCallbacks, @@ -46,39 +44,34 @@ const observer: IStoreObserverDefinition = { callbacks: {executed} } = getState(); - function applyProps( - id: any, - updatedProps: any, - enable_persistence: boolean - ) { + function applyProps(id: any, updatedProps: any) { const {layout, paths} = getState(); const itempath = getPath(paths, id); if (!itempath) { return false; } - // This is a callback-generated update. - // Check if this invalidates existing persisted prop values, - // or if persistence changed, whether this updates other props. - updatedProps = prunePersistence( - path(itempath, layout), - updatedProps, - dispatch, - enable_persistence - ); + let props = updatedProps; + + if (id === '_pages_content') { + // Check if this invalidates existing persisted prop values, + // or if persistence changed, whether this updates other props. + updatedProps = prunePersistence( + path(itempath, layout), + updatedProps, + dispatch + ); - const {props} = applyPersistence( - {props: updatedProps}, - dispatch, - enable_persistence - ); + // In case the update contains whole components, see if any of + // those components have props to update to persist user edits. + props = applyPersistence({props: updatedProps}, dispatch).props; + } - (dispatch as ThunkDispatch)( + dispatch( updateProps({ itempath, props, - source: 'response', - enable_persistence + source: 'response' }) ); @@ -112,17 +105,8 @@ const observer: IStoreObserverDefinition = { paths: oldPaths } = getState(); - const enable_persistence = - cb.callback.enable_persistence === undefined - ? false - : cb.callback.enable_persistence; - // Components will trigger callbacks on their own as required (eg. derived) - const appliedProps = applyProps( - parsedId, - props, - enable_persistence - ); + const appliedProps = applyProps(parsedId, props); // Add callbacks for modified inputs requestedCallbacks = concat( diff --git a/dash/dash-renderer/src/persistence.js b/dash/dash-renderer/src/persistence.js index 6cdbd2fe88..2b343147b9 100644 --- a/dash/dash-renderer/src/persistence.js +++ b/dash/dash-renderer/src/persistence.js @@ -364,8 +364,8 @@ export function recordUiEdit(layout, newProps, dispatch) { * Used for entire layouts (on load) or partial layouts (from children * callbacks) to apply previously-stored UI edits to components */ -export function applyPersistence(layout, dispatch, enable_persistence) { - if (type(layout) !== 'Object' || !layout.props || enable_persistence) { +export function applyPersistence(layout, dispatch) { + if (type(layout) !== 'Object' || !layout.props) { return layout; } @@ -460,12 +460,7 @@ function persistenceMods(layout, component, path, dispatch) { * these override UI-driven edits of those exact props * but not for props nested inside children */ -export function prunePersistence( - layout, - newProps, - dispatch, - enable_persistence -) { +export function prunePersistence(layout, newProps, dispatch) { const { canPersist, id, @@ -480,11 +475,7 @@ export function prunePersistence( propName in newProps ? newProps[propName] : prevVal; const finalPersistence = getFinal('persistence', persistence); - if ( - !canPersist || - !(persistence || finalPersistence) || - enable_persistence - ) { + if (!canPersist || !(persistence || finalPersistence)) { return newProps; } diff --git a/dash/dash-renderer/src/types/callbacks.ts b/dash/dash-renderer/src/types/callbacks.ts index 0a48cb4b51..d1cf636776 100644 --- a/dash/dash-renderer/src/types/callbacks.ts +++ b/dash/dash-renderer/src/types/callbacks.ts @@ -15,7 +15,6 @@ export interface ICallbackDefinition { dynamic_creator?: boolean; running: any; no_output?: boolean; - enable_persistence?: boolean; } export interface ICallbackProperty { From a353b679a22215fd426a34ce4eeb5abc5773d53d Mon Sep 17 00:00:00 2001 From: petar-qb Date: Mon, 17 Feb 2025 14:13:57 +0100 Subject: [PATCH 5/6] npm build:js error fixed --- dash/dash-renderer/src/observers/executedCallbacks.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dash/dash-renderer/src/observers/executedCallbacks.ts b/dash/dash-renderer/src/observers/executedCallbacks.ts index 3c1d019f16..00799ea0d7 100644 --- a/dash/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash/dash-renderer/src/observers/executedCallbacks.ts @@ -11,6 +11,9 @@ import { pathOr } from 'ramda'; +import {ThunkDispatch} from 'redux-thunk'; +import {AnyAction} from 'redux'; + import {IStoreState} from '../store'; import { @@ -67,7 +70,7 @@ const observer: IStoreObserverDefinition = { props = applyPersistence({props: updatedProps}, dispatch).props; } - dispatch( + (dispatch as ThunkDispatch)( updateProps({ itempath, props, From 4bb1abfe10d14501fa99e30351f0d6ebb2d5d5aa Mon Sep 17 00:00:00 2001 From: petar-qb Date: Wed, 19 Feb 2025 13:38:12 +0100 Subject: [PATCH 6/6] Fix storing only a newVal for examples like dbc.Tab --- dash/dash-renderer/src/persistence.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/dash/dash-renderer/src/persistence.js b/dash/dash-renderer/src/persistence.js index 2b343147b9..d66b9e336f 100644 --- a/dash/dash-renderer/src/persistence.js +++ b/dash/dash-renderer/src/persistence.js @@ -334,28 +334,31 @@ export function recordUiEdit(layout, newProps, dispatch) { : extract(props[propName]); let newVal = extract(newProps[propName]); - storage.setItem(valsKey, [newVal, originalVal], dispatch); + const vals = + originalVal === undefined + ? [newVal] + : [newVal, originalVal]; + + storage.setItem(valsKey, vals, dispatch); } }, persisted_props); } // Recursively record UI edits for children const {children} = props; + if (!newProps?.children) { + return; + } if (Array.isArray(children)) { + if (children.length !== newProps['children'].length) { + return; + } children.forEach((child, i) => { - if ( - type(child) === 'Object' && - child.props && - newProps['children'] !== undefined - ) { + if (type(child) === 'Object' && child.props) { recordUiEdit(child, newProps['children'][i]['props'], dispatch); } }); - } else if ( - type(children) === 'Object' && - children.props && - newProps['children'] !== undefined - ) { + } else if (type(children) === 'Object' && children.props) { recordUiEdit(children, newProps['children']['props'], dispatch); } }