I had to write my own simplified code for a persistReducer
method which allows nested blacklisting/whitelisting with the help of dot-prop-immutable and the deep merge function in this question. thanks to Salakar and CpILL
import dotProp from "dot-prop-immutable";
const STORAGE_PREFIX: string = 'persist:';
interface persistConfig {
key: string;
whitelist?: string[];
blacklist?: string[];
}
function isObject(item:any) {
return (item && typeof item === 'object' && !Array.isArray(item));
}
function mergeDeep(target:any, source:any) {
let output = Object.assign({}, target);
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach(key => {
if (isObject(source[key])) {
if (!(key in target))
Object.assign(output, { [key]: source[key] });
else
output[key] = mergeDeep(target[key], source[key]);
} else {
Object.assign(output, { [key]: source[key] });
}
});
}
return output;
}
function filterState({ state, whitelist, blacklist }: { state: any, whitelist?: string[], blacklist?: string[] }) {
if (whitelist && blacklist) {
throw Error("Can't set both whitelist and blacklist at the same time");
}
if (whitelist) {
var newState: any = {};
for (const i in whitelist) {
let val = dotProp.get(state, whitelist[i]);
if (val !== undefined) {
newState = dotProp.set(newState, whitelist[i], val)
}
}
return newState;
}
if (blacklist) {
var filteredState: any = JSON.parse(JSON.stringify(state));
for (const i in blacklist) {
filteredState = dotProp.delete(filteredState, blacklist[i]);
}
return filteredState;
}
return state
}
export function persistReducer(config: persistConfig, reducer: any) {
const { key, whitelist, blacklist } = config;
var restore_complete = false;
return (state: any, action: { type: string, payload?: any }) => {
const newState = reducer(state, action)
if (action.type === '@@INIT' && !restore_complete) {
restore_complete = true;
const data = localStorage.getItem(STORAGE_PREFIX + key);
if (data !== null) {
const newData = mergeDeep(newState, JSON.parse(data));
console.log("Restoring data:", data ,"\nnewData: ", newData);
return newData;
}
}
if(restore_complete){
const filteredNewState = filterState({
state: newState,
whitelist,
blacklist
})
localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(filteredNewState));
}
return newState;
}
}
Usage:
Same as the persistReducer function in redux-persist except that no storage options. it always uses localStorage and it accepts dotted paths in both the whitelist and blacklist parameters
if you do something like:
const persistedReducer = persistReducer (
{
key: 'theme',
whitelist: ["current_theme.palette.mode"]
},
myReducer
);
Then current_theme.palette.mode
would always be saved permanently. While any other props in the store, under current_theme, or under palette will remain intact.
Note: All you have to do to use this state persistence code is to pass your reducer function through the persistReducer
. No additional configurations such as creating a persister, from the store and wrapping your app in a PersistGate. No need to install any packages other than dot-prop-immutable, Just use the persistedReducer of your original reducer as returned by persistReducer
and you're good to go.
Note: If a default value is provided to your original reducer and some state has been saved from a previous session, both will be deeply merged when while the initial state is being loaded, with the persisted state from the previous session having higher priority so it can overwrite default values.