Redux-persist: [redux-persist]How to update persistReducer which is stored in localstorage

Created on 26 May 2020  路  6Comments  路  Source: rt2zz/redux-persist

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 ?

Most helpful comment

Simple answer is:
remove/clear storage manually and then refresh page, it will add your updated key value pair

All 6 comments

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 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 };
}

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: () => ({}),
};
Was this page helpful?
0 / 5 - 0 ratings

Related issues

scic picture scic  路  3Comments

ejbp picture ejbp  路  3Comments

thenewt15 picture thenewt15  路  3Comments

elado picture elado  路  4Comments

umairfarooq44 picture umairfarooq44  路  3Comments