rt2zz / redux-persist

persist and rehydrate a redux store
MIT License
12.96k stars 865 forks source link

[redux-persist]How to update persistReducer which is stored in localstorage #1206

Open vidhyeshpatil opened 4 years ago

vidhyeshpatil commented 4 years ago

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 ?

wmonecke commented 4 years ago

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 };
}
vidhyeshpatil commented 4 years ago

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.

wmonecke commented 4 years ago

Hmm migrations from redux-persist are def the way to go. I would encourage you to stick to this method. All the best!

shahmir811 commented 4 years ago

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

danielm2402 commented 4 years ago

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

DZakh commented 3 years ago

Simple answer is:

export const migrations = {
  16: () => ({}),
};
unigazer commented 2 years ago

I tried this solution from the official documentation https://github.com/rt2zz/redux-persist/blob/master/docs/migrations.md#alternative and it worked without manually updating the persistConfig version every time.

amitpatil321 commented 2 years ago

@unigazer : Can you describe solution in little more details? Instead or writing migrations like @wmonecke did, we just have to write this code? migrate: (state) => { console.log('Migration Running!') return Promise.resolve(state) } I didn't understand whats given on that link :/

Death-thekidd commented 1 year ago

Yes @amitpatil321 I just tested it and it woks for me

DatHip commented 1 year ago

Tôi đã thử giải pháp này từ tài liệu chính thức https://github.com/rt2zz/redux-persist/blob/master/docs/migrations.md#alternative và nó hoạt động mà không cần cập nhật persistConfigphiên bản theo cách thủ công mỗi lần.

I used this method fine, but now it's not working

asamad35 commented 1 year ago

Migration can be a way. But I created a function to check the number of keys in reducer with a hard coded value. If they are not equal logout and clear the persisted state in local storage and also do not render the children. You can run this in useEffect hook. This function needs to run at the top level of application (index.js or app.js in react) .


    useEffect(() => {
    const NO_OF_REDUX_STATE_KEYS = 1

      //1) if we add new properties in redux it won't be avalaible in the already loggedin user's redux-persist state, so user needs to login again to maintain the state.

if (Object.keys(reduxState).length !== NO_OF_REDUX_STATE_KEYS) {
      setShowChildren(false)
      localStorage.removeItem('persist:Hoichoi')
      router.push('/') // login page
    } else {
        setShowChildren(true)
    }
  }
alamenai commented 5 months ago

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

What should be the type of the state in TypeScript , RootState, right?

   0: (state) => {
     return {
       ...state,
       reducerA: {
         ...state.reducerA, // number: 0
        checkPage: [],
       }
     }
   },
};