Skip to content

Commit d6cbee8

Browse files
authored
Merge pull request #3279 from petar-qb/fix/store-output-value-in-persistence-storage
Store callback Output value in persistence storage
2 parents 8ee6a46 + fc5eea6 commit d6cbee8

File tree

5 files changed

+44
-48
lines changed

5 files changed

+44
-48
lines changed

CHANGELOG.md

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

5+
6+
## [UNRELEASED]
7+
8+
## Fixed
9+
- [#3279](https://github.com/plotly/dash/pull/3279) Fix an issue where persisted values were incorrectly pruned when updated via callback. Now, callback returned values are correctly stored in the persistence storage. Fix [#2678](https://github.com/plotly/dash/issues/2678)
10+
511
## [3.0.4] - 2025-04-24
612

713
## Fixed

dash/dash-renderer/src/actions/index.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {once} from 'ramda';
1+
import {once, path} from 'ramda';
22
import {createAction} from 'redux-actions';
33
import {addRequestedCallbacks} from './callbacks';
44
import {getAppState} from '../reducers/constants';
@@ -7,6 +7,7 @@ import cookie from 'cookie';
77
import {validateCallbacksToLayout} from './dependencies';
88
import {includeObservers, getLayoutCallbacks} from './dependencies_ts';
99
import {computePaths, getPath} from './paths';
10+
import {recordUiEdit} from '../persistence';
1011

1112
export const onError = createAction(getAction('ON_ERROR'));
1213
export const setAppLifecycle = createAction(getAction('SET_APP_LIFECYCLE'));
@@ -17,10 +18,19 @@ export const setHooks = createAction(getAction('SET_HOOKS'));
1718
export const setLayout = createAction(getAction('SET_LAYOUT'));
1819
export const setPaths = createAction(getAction('SET_PATHS'));
1920
export const setRequestQueue = createAction(getAction('SET_REQUEST_QUEUE'));
20-
export const updateProps = createAction(getAction('ON_PROP_CHANGE'));
2121
export const insertComponent = createAction(getAction('INSERT_COMPONENT'));
2222
export const removeComponent = createAction(getAction('REMOVE_COMPONENT'));
2323

24+
export const onPropChange = createAction(getAction('ON_PROP_CHANGE'));
25+
26+
export function updateProps(payload) {
27+
return (dispatch, getState) => {
28+
const component = path(payload.itempath, getState().layout);
29+
recordUiEdit(component, payload.props, dispatch);
30+
dispatch(onPropChange(payload));
31+
};
32+
}
33+
2434
export const addComponentToLayout = payload => (dispatch, getState) => {
2535
const {paths} = getState();
2636
dispatch(insertComponent(payload));

dash/dash-renderer/src/observers/executedCallbacks.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import {
1111
pathOr
1212
} from 'ramda';
1313

14+
import {ThunkDispatch} from 'redux-thunk';
15+
import {AnyAction} from 'redux';
16+
1417
import {IStoreState} from '../store';
1518

1619
import {
@@ -63,7 +66,7 @@ const observer: IStoreObserverDefinition<IStoreState> = {
6366
// In case the update contains whole components, see if any of
6467
// those components have props to update to persist user edits.
6568
const {props} = applyPersistence({props: updatedProps}, dispatch);
66-
dispatch(
69+
(dispatch as ThunkDispatch<any, any, AnyAction>)(
6770
updateProps({
6871
itempath,
6972
props,

dash/dash-renderer/src/persistence.js

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,14 @@ export function recordUiEdit(layout, newProps, dispatch) {
318318
persisted_props,
319319
persistence_type
320320
} = getProps(layout);
321-
if (!canPersist || !persistence) {
321+
322+
// if the "persistence" property is changed as a callback output,
323+
// skip the persistence storage overwriting.
324+
const isPersistenceMismatch =
325+
newProps?.persistence !== undefined &&
326+
newProps.persistence !== persistence;
327+
328+
if (!canPersist || !persistence || isPersistenceMismatch) {
322329
return;
323330
}
324331

@@ -501,46 +508,21 @@ export function prunePersistence(layout, newProps, dispatch) {
501508
depersistedProps = mergeRight(props, update);
502509
}
503510

504-
if (finalPersistence) {
511+
if (finalPersistence && persistenceChanged) {
505512
const finalStorage = getStore(finalPersistenceType, dispatch);
506-
507-
if (persistenceChanged) {
508-
// apply new persistence
509-
forEach(
510-
persistedProp =>
511-
modProp(
512-
getValsKey(id, persistedProp, finalPersistence),
513-
finalStorage,
514-
element,
515-
depersistedProps,
516-
persistedProp,
517-
update
518-
),
519-
filter(notInNewProps, finalPersistedProps)
520-
);
521-
}
522-
523-
// now the main point - clear any edit of a prop that changed
524-
// note that this is independent of the new prop value.
525-
const transforms = element.persistenceTransforms || {};
526-
for (const propName in newProps) {
527-
const propTransforms = transforms[propName];
528-
if (propTransforms) {
529-
for (const propPart in propTransforms) {
530-
finalStorage.removeItem(
531-
getValsKey(
532-
id,
533-
`${propName}.${propPart}`,
534-
finalPersistence
535-
)
536-
);
537-
}
538-
} else {
539-
finalStorage.removeItem(
540-
getValsKey(id, propName, finalPersistence)
541-
);
542-
}
543-
}
513+
// apply new persistence
514+
forEach(
515+
persistedProp =>
516+
modProp(
517+
getValsKey(id, persistedProp, finalPersistence),
518+
finalStorage,
519+
element,
520+
depersistedProps,
521+
persistedProp,
522+
update
523+
),
524+
filter(notInNewProps, finalPersistedProps)
525+
);
544526
}
545527
return persistenceChanged ? mergeRight(newProps, update) : newProps;
546528
}

dash/dash-renderer/src/wrapper/DashWrapper.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import {DashLayoutPath, UpdatePropsPayload} from '../types/component';
2323
import {DashConfig} from '../config';
2424
import {notifyObservers, onError, updateProps} from '../actions';
2525
import {getWatchedKeys, stringifyId} from '../actions/dependencies';
26-
import {recordUiEdit} from '../persistence';
2726
import {
2827
createElement,
2928
getComponentLayout,
@@ -132,10 +131,6 @@ function DashWrapper({
132131
const watchedKeys = getWatchedKeys(id, keys(changedProps), graphs);
133132

134133
batch(() => {
135-
// setProps here is triggered by the UI - record these changes
136-
// for persistence
137-
recordUiEdit(renderComponent, newProps, dispatch);
138-
139134
// Only dispatch changes to Dash if a watched prop changed
140135
if (watchedKeys.length) {
141136
dispatch(

0 commit comments

Comments
 (0)