npm install @menardi/redux-persist
A fork of redux-persist (forked from v6.0.0) which is focused on more reliable usage as a permanent store rather than a cache.
I use this fork for my mobile apps, so my main goal is to ensure it stores data reliably inside a mobile app like React Native or Capacitor.
- Throws errors instead of silently deleting data when something goes wrong
- State reconciliation defaults to level 2, which means that adding new keys inside a reducer with default values works as expected
- Support for Redux v5 and @reduxjs/toolkit
- The
stateReconcilerconfig option has been removed, and replaced withrehydrationDepthstateReconciler: autoMergeLevel1->rehydrationDepth: 1stateReconciler: autoMergeLevel2->rehydrationDepth: 2stateReconciler: hardSet-> removed
- The
whitelistandblacklistconfig options have been renamed, but the old names are still supported for backwards compatibilitywhitelist->allowlistblacklist->blocklist
persistCombineReducershas been removed- If you were using this, separately call
combineReducersandpersistReducerinstead
- If you were using this, separately call
- The
redux-persist/lib/storageexport has been removed- This offered browser-specific storage. Instead, set
storage: localStorageorstorage: sessionStoragedirectly
- This offered browser-specific storage. Instead, set
- The deprecated
keyPrefixconfig option has been removed - The
createTransformfunction now takes an object argumentcreateTransform(in, out, { whitelist: ['myReducer'] })->createTransform({ reducerName: 'myReducer', onBeforePersist: in, onBeforeRehydrate: out })
Basic usage involves adding persistReducer and persistStore to your setup.
// configureStore.ts
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { persistReducer, persistStore, ALL_PERSIST_ACTIONS } from '@menardi/redux-persist';
const rootReducer = combineReducers({
// your reducers here
});
const persistedReducer = persistReducer({
key: 'root',
storage: MyStorage,
}, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) => (
getDefaultMiddleware({
serializableCheck: {
// This tells redux-toolkit to ignore its serializability check for redux-persist's internal actions
ignoredActions: ALL_PERSIST_ACTIONS,
},
})
),
});
export const persistor = persistStore(store);If you are using React, wrap your root component with PersistGate. This delays the rendering of your app's UI until your persisted state has been retrieved and saved to redux. NOTE the PersistGate loading prop can be null, or any react instance, e.g. loading={<Loading />}
import { PersistGate } from '@menardi/redux-persist/integration/react'
// ... normal setup, create store and persistor, import components etc.
const App = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<RootComponent />
</PersistGate>
</Provider>
);
};Any storage which provides getItem, setItem and removeItem functions can be used for your persistor.
If you're starting a new React Native project, react-native-mmkv is the recommended storage option due to its speed and synchronous reads.
You can create your own wrapper around react-native-mmkv to match the format that redux-persist expects.
import { createMMKV } from 'react-native-mmkv';
const mmkvStorage = createMMKV();
const PersistCompatibleMmkv = {
setItem: (key: string, value: string) => {
mmkvStorage.set(key, value);
},
getItem: (key: string) => {
return mmkvStorage.getString(key);
},
removeItem: (key: string) => {
mmkvStorage.remove(key);
},
};
const rootReducer = combineReducers({
// your reducers here
});
const persistedReducer = persistReducer({
key: 'root',
storage: PersistCompatibleMmkv,
}, rootReducer);AsyncStorage already provides the exact functions needed, and so can be passed directly to persistReducer as is.
import AsyncStorage from '@react-native-async-storage/async-storage';
const rootReducer = combineReducers({
// your reducers here
});
const persistedReducer = persistReducer({
key: 'root',
storage: AsyncStorage,
}, rootReducer);You can create your own wrapper around @capacitor/preferences to match the format that redux-persist expects.
import { Preferences } from '@capacitor/preferences'
const PersistCompatiblePreferences = {
setItem: (key: string, value: string) => {
return Preferences.set({ key, value });
},
getItem: async (key: string) => {
const result = await Preferences.get({ key });
return result.value;
},
removeItem: (key: string) => {
return Preferences.remove({ key });
},
};
const rootReducer = combineReducers({
// your reducers here
});
const persistedReducer = persistReducer({
key: 'root',
storage: PersistCompatiblePreferences,
}, rootReducer);Important
If you're using redux-persist in a hybrid mobile app (like Capacitor or Cordova), do not use localStorage.
The iOS system treats localStorage like a cache that can be cleared, so data can be lost unexpectedly.
For hybrid apps, use a native storage option, like @capacitor/preferences above.
localStorage already provides the exact functions needed, and so can be passed directly to persistReducer as is.
const rootReducer = combineReducers({
// your reducers here
});
const persistedReducer = persistReducer({
key: 'root',
storage: localStorage,
}, rootReducer);To block specific reducers from being persisted, add them to the blocklist config array. If you want to only persist specific reducers, you can use the allowlist config option.
// BLOCKLIST
const persistConfig = {
key: 'root',
storage: storage,
blocklist: ['navigation'] // navigation will not be persisted
};
// ALLOWLIST
const persistConfig = {
key: 'root',
storage: storage,
allowlist: ['navigation'] // only navigation will be persisted
};persistReducer has a general purpose "migrate" config which will be called after getting stored state but before actually reconciling with the reducer. It can be any function which takes state as an argument and returns a promise to return a new state object.
Redux Persist ships with createMigrate, which helps create a synchronous migration for moving from any version of stored state to the current state version.
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { createMigrate, persistReducer, persistStore, ALL_PERSIST_ACTIONS } from '@menardi/redux-persist';
const rootReducer = combineReducers({
// your reducers here
});
const migrations = {
0: (state) => {
// migration to delete "device"
return {
...state,
device: undefined
}
},
1: (state) => {
// migration to keep only "device"
return {
device: state.device
}
}
}
const persistedReducer = persistReducer({
key: 'root',
version: 1,
storage: MyStorage,
migrate: createMigrate(migrations),
}, rootReducer);
...The migrate method can be any function with which returns a promise of new state.
const persistedReducer = persistReducer({
key: 'root',
version: 1,
storage: MyStorage,
migrate: (state) => {
console.log('Migration Running!')
return Promise.resolve(state)
}
}, rootReducer);Transforms allow you to customize the state object that gets persisted and rehydrated.
When the state object gets persisted, it first gets serialized with JSON.stringify(). If parts of your state object are not mappable to JSON objects, the serialization process may transform these parts of your state in unexpected ways. For example, the javascript Set type does not exist in JSON. When you try to serialize a Set via JSON.stringify(), it gets converted to an empty object, which is almost definitely not what you want.
Below is a Transform that successfully persists a Set property, which simply converts it to an array and back. In this way, the Set gets converted to an Array, which is a recognized data structure in JSON. When pulled out of the persisted store, the array gets converted back to a Set before being saved to the redux store.
import { createTransform } from '@menardi/redux-persist';
const rootReducer = combineReducers({
reducerWithSet: ...
});
type RootState = ReturnType<typeof rootReducer>;
const SetTransform = createTransform<RootState, 'reducerWithSet'>({
reducerName: 'reducerWithSet',
onBeforePersist: (state) => {
// Note that `state` is the state of the specified reducer (`reducerWithSet`), not the `rootReducer`
return { ...state, mySet: [...state.mySet] };
},
onBeforeRehydrate: (state) => {
return { ...state, mySet: new Set(state.mySet) };
},
});
const persistedReducer = persistReducer({
key: 'root',
storage: MyStorage,
transforms: [SetTransform]
}, rootReducer);