Search code examples
javascripttypescriptredux-persist

whitelist nested item in state (Redux persist)


The state in my reducer contains the key current_theme, which contains an object, with the key palette, which contains an object with the key mode which can either be the string value "dark" or the string value "light"

So I need to make only this bit of data persistent while leaving all other attributes intact.

redux-persist offers a whitelist parameter which is what I want. However, I can only do something like

const persistedReducer = persistReducer (
    {
        key: 'theme',
        storage,
        whitelist: ["current_theme"]
    },
    myReducer
);

But this makes everything inside current_theme persistent. I want only current_theme.palette.mode to be persistent and nothing else.

I tried the below but it didn't work neither

const persistedReducer = persistReducer (
    {
        key: 'theme',
        storage,
        whitelist: ["current_theme.palette.mode"]
    },
    myReducer
);

Solution

  • 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.