What is the best way to update persistStore when my store / reducer is updated with new key, value (inside initial state) or a new reducer.
The problem which I am facing write now, is my persistStore is not getting updated when I update my reducer with additional value.
eg: Reducer A
Old one
const initialState = {
number: 0
}
Updated one
const initialState = {
number: 0,
checkPage: []
}
When my application runs again, it tries to access checkPage array it throws an error "Cannot read property undefined of undefined". Which means my persistedReducer was not updated.
A common use case, Can you suggest the best way to resolve the issue ?
Hi @vidhyeshpatil!
You need to create a migration and pass it to your persistConfig.
Here is an example:
````
import rootReducer from './reducers';
const migrations = {
0: (state) => {
return {
...state,
settings: {
...state.settings,
moodColorCategory: {
colorPalette: 'DEFAULT',
}
}
}
},
};
const persistConfig = {
key: 'root',
storage: AsyncStorage,
version: 0,
timeout: 0,
migrate: createMigrate(migrations, { debug: true }),
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
export default () => {
const store = createStore(persistedReducer, {}, applyMiddleware(ReduxThunk));;
const persistor = persistStore(store);
return { store, persistor };
}
````
As you can see I created a migrations object and gave it the key 0 which I then matched to the version in persistConfig.
When your app runs again it will check for the key 0 and return the state that is returned by the key 0 in migrations. In other words: whatever you return from 0 is your new state. In your case it would be something like:
const migrations = {
0: (state) => {
return {
...state,
reducerA: {
...state.reducerA, // number: 0
checkPage: [],
}
}
},
};
In the future, you would increment this number by one and on app start redux persist will check if its bigger than the current version and if it is it will migrate the new state.
This is currently my migrations object (so you get the idea):
````
import moment from 'moment';
import strings from './i18n/strings';
import ReduxThunk from 'redux-thunk';
import { createStore, applyMiddleware } from 'redux';
import AsyncStorage from '@react-native-community/async-storage';
import { persistStore, persistReducer, createMigrate } from 'redux-persist';
import { defaultActivities, ADDED_IN_VERSION_3_4 } from './assets/activities/activities';
import { INITIAL_HOME_WIDGETS, INITIAL_MENU_ITEMS, LINE_CHART_CURVE_TYPES, TIME_WINDOWS } from './constants';
import { EXTENDED_EMOTIONS_MAP_AS_ARRAY, EXTENDED_EMOTIONS_MAP, ALL_EMOTIONS, BASIC_EMOTIONS } from './assets/objectProperties/emotionsMap';
import rootReducer from './reducers';
const migrations = {
0: (state) => {
return {
...state,
settings: {
...state.settings,
moodColorCategory: {
colorPalette: 'DEFAULT',
}
}
}
},
1: (state) => {
return {
...state,
settings: {
...state.settings,
layoutCategory: {
...state.settings.layoutCategory,
moodLayout: 'GRID',
firstMonthToShow: 1,
shouldHighlightCurrentDay: true,
},
moodSelectionCategory: {
defaultAndCustomMoodsAsSingleArray: EXTENDED_EMOTIONS_MAP_AS_ARRAY,
defaultAndCustomMoodsAsSegmentedObject: EXTENDED_EMOTIONS_MAP
}
}
}
},
2: (state) => {
return {
...state,
settings: {
...state.settings,
moodColorCategory: {
...state.settings.moodColorCategory,
shouldUseDefaultColorPalettes: true,
customMoodColors_1: null,
customMoodColors_2: null,
customMoodColors_3: null,
customMoodSelected: null,
},
languagePreference: {
userEnforcedLanguage: null,
},
whatsNew: {
showWhatsNewModal: true,
}
}
}
},
3: (state) => {
return {
...state,
settings: {
...state.settings,
backgroundCategory: {
isBackgroundUsingVideo: true,
savedImage: null,
savedVideo: 'defaultVideo',
},
whatsNew: {
showWhatsNewModal: true,
},
}
}
},
4: (state) => {
return {
...state,
challenges: [],
pastChallenges: [],
googleDrive: {
userInfo: null,
lastBackupTimestamp: null,
},
settings: {
...state.settings,
whatsNew: {
showWhatsNewModal: true,
},
}
}
},
5: (state) => {
return {
...state,
userInfo: {
...state.userInfo,
lastViewedRewardedVideo: moment("January 20 2019 05:06:07", "MMMM DD YYYY hh:mm:ss"),
},
settings: {
...state.settings,
whatsNew: {
showWhatsNewModal: true,
},
}
}
},
6: (state) => {
return {
...state,
activities: defaultActivities,
pinlock: {
pin: '',
pinlockEnabled: false,
},
settings: {
...state.settings,
whatsNew: {
showWhatsNewModal: true,
},
}
}
},
7: (state) => {
return {
...state,
settings: {
...state.settings,
layoutCategory: {
...state.settings.layoutCategory,
shouldAllowFloatingButton: true,
shouldAllowWhiteBorders: false,
shouldAllowGradients: true,
firstMonthToShow: 1,
},
whatsNew: {
showWhatsNewModal: true,
},
}
}
},
8: (state) => {
return {
...state,
settings: {
...state.settings,
layoutCategory: {
...state.settings.layoutCategory,
shouldAllowWhiteBorders: false,
shouldAllowGradients: true,
firstMonthToShow: 1,
shouldAllowActivities: true,
shouldAllowLocation: true,
},
whatsNew: {
showWhatsNewModal: true,
},
},
// open street maps
locationOSM: {
locationName: null,
openStreetMapId: null,
latitude: 85.287973,
longitude: 36.198180,
timestamp: moment("January 20 2019 05:06:07", "MMMM DD YYYY hh:mm:ss"),
},
// google maps
locationGM: {
cachedPredictions: [],
timestamp: moment("January 20 2019 05:06:07", "MMMM DD YYYY hh:mm:ss"),
},
// custom location
locationCL: {
cachedCustomLocations: [],
}
}
},
9: (state) => {
return {
...state,
settings: {
...state.settings,
backgroundCategory: {
...state.settings.backgroundCategory,
isBackgroundImageCustom: false,
},
whatsNew: {
showWhatsNewModal: true,
},
},
}
},
10: (state) => {
return {
...state,
settings: {
...state.settings,
layoutCategory: {
...state.settings.layoutCategory,
shouldAllowFloatingButton: true,
shouldAllowDotStyle: false,
dotStylePosition: 'right',
},
whatsNew: {
showWhatsNewModal: true,
},
migration: {
hasMigratedToMultipleEntries: false,
}
},
}
},
11: (state) => {
return {
...state,
extraNotifications: [],
settings: {
...state.settings,
layoutCategory: {
...state.settings.layoutCategory,
shouldAllowFloatingEntryButton: true,
},
ratingDescribers: {
happyMood: strings.EmotionsLayoutDescribers.basicMoods.happyMood,
contentMood: strings.EmotionsLayoutDescribers.basicMoods.contentMood,
neutralMood: strings.EmotionsLayoutDescribers.basicMoods.neutralMood,
sadMood: strings.EmotionsLayoutDescribers.basicMoods.sadMood,
depressedMood: strings.EmotionsLayoutDescribers.basicMoods.depressedMood,
},
whatsNew: {
showWhatsNewModal: true,
},
},
}
},
12: (state) => {
return {
...state,
settings: {
...state.settings,
layoutCategory: {
...state.settings.layoutCategory,
weekStartsOnMonday: true,
},
whatsNew: {
showWhatsNewModal: true,
},
},
}
},
13: (state) => {
return {
...state,
inAppReviews: {
rating: null,
feedback: '',
timestamp: null,
},
firebaseBackup: {
lastBackupTimestamp: null,
},
imageBackupTracker: {
toUpload: {},
toDelete: [],
},
userInfo: {
...state.userInfo,
birthday: null,
},
settings: {
...state.settings,
layoutCategory: {
...state.settings.layoutCategory,
shouldAllowPhotos: true,
shouldAllowCalendarFullWidth: false,
googleColorScheme: 'Standard',
},
notificationCategory: {
...state.settings.notificationCategory,
reminderNotifMinute: 0,
},
whatsNew: {
showWhatsNewModal: true,
},
tipsAndTricks: {
hasSeenPhotosMessage: false,
},
},
map: {
initialRegion: null,
},
autoSaveEntry: {
entryText: '',
timestamp: new Date().getTime(),
},
routines: {
activeRoutines: [],
routineProgress: {},
deletedRoutines: [],
customRoutineItems: [],
},
subscriptions: {
purchases: {},
lastValidationCheck: null,
isSubscriptionActive: false,
latestPurchasedProduct: null,
},
activities: [
...state.activities,
...ADDED_IN_VERSION_3_4,
]
}
},
14: (state) => {
return {
...state,
settings: {
...state.settings,
weather: {
allowWeather: true,
unitOfMeasurement: 'metric', // us_custom
},
tipsAndTricks: {
...state.settings.tipsAndTricks,
hasSeenMultipleEntriesMessage: false,
}
},
}
},
15: (state) => {
return {
...state,
settings: {
...state.settings,
tipsAndTricks: {
...state.settings.tipsAndTricks,
hasSeenMonthStatisticMessage: false,
},
hapticFeedback: {
allowHapticFeedback: false,
},
},
}
},
16: (state) => {
return {
...state,
settings: {
...state.settings,
layoutCategory: {
...state.settings.layoutCategory,
currentActiveJournalTab: 'entry',
backgroundOpacity: .72,
elementBackgroundColor: 100,
isShowingCurrentWeek: false,
monthView: 'default',
menuItems: [...INITIAL_MENU_ITEMS],
homeComponentWidgets: [...INITIAL_HOME_WIDGETS],
},
migration: {
...state.migration,
hasMigratedToAccurateTimestamps: false,
},
insightCategory: {
ratingLineChartCurve: LINE_CHART_CURVE_TYPES[0], // step or average
timeWindowToDisplay: TIME_WINDOWS[0],
},
},
notifications: {},
gratitudeJournal: {},
quotes: {
todaysQuote: {
...strings.Quotes[0],
dateSet: new Date().getTime(),
},
likedQuotes: [],
},
emotions: {
allEmotions: [...ALL_EMOTIONS],
basicEmotions: [...BASIC_EMOTIONS],
},
};
}
};
const persistConfig = {
key: 'root',
storage: AsyncStorage,
version: 16,
timeout: 0,
migrate: createMigrate(migrations, { debug: true }),
writeFailHandler: error => console.log('ERROR PERSISTING DATA', error),
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
export default () => {
const store = createStore(persistedReducer, {}, applyMiddleware(ReduxThunk));;
const persistor = persistStore(store);
return { store, persistor };
}
````
Hi @vidhyeshpatil!
You need to create a migration and pass it to your
persistConfig.Here is an example:
import rootReducer from './reducers'; const migrations = { 0: (state) => { return { ...state, settings: { ...state.settings, moodColorCategory: { colorPalette: 'DEFAULT', } } } }, }; const persistConfig = { key: 'root', storage: AsyncStorage, version: 0, timeout: 0, migrate: createMigrate(migrations, { debug: true }), }; const persistedReducer = persistReducer(persistConfig, rootReducer); export default () => { const store = createStore(persistedReducer, {}, applyMiddleware(ReduxThunk));; const persistor = persistStore(store); return { store, persistor }; }As you can see I created a
migrationsobject and gave it the key0which I then matched to theversioninpersistConfig.When your app runs again it will check for the key 0 and return the
statethat is returned by the key0inmigrations. In other words: whatever you return from0is your new state. In your case it would be something like:const migrations = { 0: (state) => { return { ...state, reducerA: { ...state.reducerA, // number: 0 checkPage: [], } } }, };In the future, you would increment this number by one and on app start redux persist will check if its bigger than the current version and if it is it will migrate the new state.
This is currently my
migrationsobject (so you get the idea):import moment from 'moment'; import strings from './i18n/strings'; import ReduxThunk from 'redux-thunk'; import { createStore, applyMiddleware } from 'redux'; import AsyncStorage from '@react-native-community/async-storage'; import { persistStore, persistReducer, createMigrate } from 'redux-persist'; import { defaultActivities, ADDED_IN_VERSION_3_4 } from './assets/activities/activities'; import { INITIAL_HOME_WIDGETS, INITIAL_MENU_ITEMS, LINE_CHART_CURVE_TYPES, TIME_WINDOWS } from './constants'; import { EXTENDED_EMOTIONS_MAP_AS_ARRAY, EXTENDED_EMOTIONS_MAP, ALL_EMOTIONS, BASIC_EMOTIONS } from './assets/objectProperties/emotionsMap'; import rootReducer from './reducers'; const migrations = { 0: (state) => { return { ...state, settings: { ...state.settings, moodColorCategory: { colorPalette: 'DEFAULT', } } } }, 1: (state) => { return { ...state, settings: { ...state.settings, layoutCategory: { ...state.settings.layoutCategory, moodLayout: 'GRID', firstMonthToShow: 1, shouldHighlightCurrentDay: true, }, moodSelectionCategory: { defaultAndCustomMoodsAsSingleArray: EXTENDED_EMOTIONS_MAP_AS_ARRAY, defaultAndCustomMoodsAsSegmentedObject: EXTENDED_EMOTIONS_MAP } } } }, 2: (state) => { return { ...state, settings: { ...state.settings, moodColorCategory: { ...state.settings.moodColorCategory, shouldUseDefaultColorPalettes: true, customMoodColors_1: null, customMoodColors_2: null, customMoodColors_3: null, customMoodSelected: null, }, languagePreference: { userEnforcedLanguage: null, }, whatsNew: { showWhatsNewModal: true, } } } }, 3: (state) => { return { ...state, settings: { ...state.settings, backgroundCategory: { isBackgroundUsingVideo: true, savedImage: null, savedVideo: 'defaultVideo', }, whatsNew: { showWhatsNewModal: true, }, } } }, 4: (state) => { return { ...state, challenges: [], pastChallenges: [], googleDrive: { userInfo: null, lastBackupTimestamp: null, }, settings: { ...state.settings, whatsNew: { showWhatsNewModal: true, }, } } }, 5: (state) => { return { ...state, userInfo: { ...state.userInfo, lastViewedRewardedVideo: moment("January 20 2019 05:06:07", "MMMM DD YYYY hh:mm:ss"), }, settings: { ...state.settings, whatsNew: { showWhatsNewModal: true, }, } } }, 6: (state) => { return { ...state, activities: defaultActivities, pinlock: { pin: '', pinlockEnabled: false, }, settings: { ...state.settings, whatsNew: { showWhatsNewModal: true, }, } } }, 7: (state) => { return { ...state, settings: { ...state.settings, layoutCategory: { ...state.settings.layoutCategory, shouldAllowFloatingButton: true, shouldAllowWhiteBorders: false, shouldAllowGradients: true, firstMonthToShow: 1, }, whatsNew: { showWhatsNewModal: true, }, } } }, 8: (state) => { return { ...state, settings: { ...state.settings, layoutCategory: { ...state.settings.layoutCategory, shouldAllowWhiteBorders: false, shouldAllowGradients: true, firstMonthToShow: 1, shouldAllowActivities: true, shouldAllowLocation: true, }, whatsNew: { showWhatsNewModal: true, }, }, // open street maps locationOSM: { locationName: null, openStreetMapId: null, latitude: 85.287973, longitude: 36.198180, timestamp: moment("January 20 2019 05:06:07", "MMMM DD YYYY hh:mm:ss"), }, // google maps locationGM: { cachedPredictions: [], timestamp: moment("January 20 2019 05:06:07", "MMMM DD YYYY hh:mm:ss"), }, // custom location locationCL: { cachedCustomLocations: [], } } }, 9: (state) => { return { ...state, settings: { ...state.settings, backgroundCategory: { ...state.settings.backgroundCategory, isBackgroundImageCustom: false, }, whatsNew: { showWhatsNewModal: true, }, }, } }, 10: (state) => { return { ...state, settings: { ...state.settings, layoutCategory: { ...state.settings.layoutCategory, shouldAllowFloatingButton: true, shouldAllowDotStyle: false, dotStylePosition: 'right', }, whatsNew: { showWhatsNewModal: true, }, migration: { hasMigratedToMultipleEntries: false, } }, } }, 11: (state) => { return { ...state, extraNotifications: [], settings: { ...state.settings, layoutCategory: { ...state.settings.layoutCategory, shouldAllowFloatingEntryButton: true, }, ratingDescribers: { happyMood: strings.EmotionsLayoutDescribers.basicMoods.happyMood, contentMood: strings.EmotionsLayoutDescribers.basicMoods.contentMood, neutralMood: strings.EmotionsLayoutDescribers.basicMoods.neutralMood, sadMood: strings.EmotionsLayoutDescribers.basicMoods.sadMood, depressedMood: strings.EmotionsLayoutDescribers.basicMoods.depressedMood, }, whatsNew: { showWhatsNewModal: true, }, }, } }, 12: (state) => { return { ...state, settings: { ...state.settings, layoutCategory: { ...state.settings.layoutCategory, weekStartsOnMonday: true, }, whatsNew: { showWhatsNewModal: true, }, }, } }, 13: (state) => { return { ...state, inAppReviews: { rating: null, feedback: '', timestamp: null, }, firebaseBackup: { lastBackupTimestamp: null, }, imageBackupTracker: { toUpload: {}, toDelete: [], }, userInfo: { ...state.userInfo, birthday: null, }, settings: { ...state.settings, layoutCategory: { ...state.settings.layoutCategory, shouldAllowPhotos: true, shouldAllowCalendarFullWidth: false, googleColorScheme: 'Standard', }, notificationCategory: { ...state.settings.notificationCategory, reminderNotifMinute: 0, }, whatsNew: { showWhatsNewModal: true, }, tipsAndTricks: { hasSeenPhotosMessage: false, }, }, map: { initialRegion: null, }, autoSaveEntry: { entryText: '', timestamp: new Date().getTime(), }, routines: { activeRoutines: [], routineProgress: {}, deletedRoutines: [], customRoutineItems: [], }, subscriptions: { purchases: {}, lastValidationCheck: null, isSubscriptionActive: false, latestPurchasedProduct: null, }, activities: [ ...state.activities, ...ADDED_IN_VERSION_3_4, ] } }, 14: (state) => { return { ...state, settings: { ...state.settings, weather: { allowWeather: true, unitOfMeasurement: 'metric', // us_custom }, tipsAndTricks: { ...state.settings.tipsAndTricks, hasSeenMultipleEntriesMessage: false, } }, } }, 15: (state) => { return { ...state, settings: { ...state.settings, tipsAndTricks: { ...state.settings.tipsAndTricks, hasSeenMonthStatisticMessage: false, }, hapticFeedback: { allowHapticFeedback: false, }, }, } }, 16: (state) => { return { ...state, settings: { ...state.settings, layoutCategory: { ...state.settings.layoutCategory, currentActiveJournalTab: 'entry', backgroundOpacity: .72, elementBackgroundColor: 100, isShowingCurrentWeek: false, monthView: 'default', menuItems: [...INITIAL_MENU_ITEMS], homeComponentWidgets: [...INITIAL_HOME_WIDGETS], }, migration: { ...state.migration, hasMigratedToAccurateTimestamps: false, }, insightCategory: { ratingLineChartCurve: LINE_CHART_CURVE_TYPES[0], // step or average timeWindowToDisplay: TIME_WINDOWS[0], }, }, notifications: {}, gratitudeJournal: {}, quotes: { todaysQuote: { ...strings.Quotes[0], dateSet: new Date().getTime(), }, likedQuotes: [], }, emotions: { allEmotions: [...ALL_EMOTIONS], basicEmotions: [...BASIC_EMOTIONS], }, }; } }; const persistConfig = { key: 'root', storage: AsyncStorage, version: 16, timeout: 0, migrate: createMigrate(migrations, { debug: true }), writeFailHandler: error => console.log('ERROR PERSISTING DATA', error), }; const persistedReducer = persistReducer(persistConfig, rootReducer); export default () => { const store = createStore(persistedReducer, {}, applyMiddleware(ReduxThunk));; const persistor = persistStore(store); return { store, persistor }; }
Thanks for your detailed description, appreciated.
But maintaining lot of version in future, would be a one more hectic step. Also as per your code example shared by you setting a timeout 0 the application doesn't start it just displays my loader, the state doesn't get rehydrated.
Setting mirgrations object, it doesn't reflect when I am tried to implement & run the application local. It doesn't updates the new additional value.
Any better alternative, which we can do to achieve this scenario, because if I want to add new additional reducer that time also it will result an issue.
Hmm migrations from redux-persist are def the way to go. I would encourage you to stick to this method. All the best!
Simple answer is:
remove/clear storage manually and then refresh page, it will add your updated key value pair
La respuesta simple es:
elimine / borre el almacenamiento manualmente y luego actualice la p谩gina, agregar谩 su par de valor clave actualizado
How would I do if my project is in production? it's a bit annoying to ask the user to clean up their local storage
Simple answer is:
export const migrations = {
16: () => ({}),
};
Most helpful comment
Simple answer is:
remove/clear storage manually and then refresh page, it will add your updated key value pair