DEV Community

Cover image for Recursive Loop in Redux Reducer to check integrity of local Storage
chowderhead
chowderhead

Posted on • Edited on

Recursive Loop in Redux Reducer to check integrity of local Storage

The Problem:

When we would add nested Settings inside of our local storage object, users were getting errors that were pointing to our MapStateToProps, the keys in their local Storage were missing our new updates.

the Error:

WHy?!?!?

Our Local Storage Object:

we used local storage to save things like tab state , and in this instance we saved what was the last cover page / header you created in your last document.

reducers/local-storage-settings.js


const DEFAULT_SETTINGS = {
  docs: {
    settings: {
      coverPageId: null,
      headerId: null
    }
  },
  projectInfoOpen: true,
  lastSelectedInfoTab: null,
  lastSelectedAboutTab: null,
  lastSelectedContactTab: null,
  lastSelectedRegisterTab: null,

};

Enter fullscreen mode Exit fullscreen mode

When a user logs in to our application we check their local storage and set Our redux store equal to the object we saved into their local storage during their last visit.

The issue is when we add new fields to this object, we want to have nested fields so that it looks pretty right?

But if we have nested fields , we want to be able to check to make sure those keys exist

When?!

Well since this is dealing with the state , we do not want this function running all the time, the best place I could think of was putting it on the componentWillMount inside of our base container inside of the project. (we had a local storage object for each project)


  componentWillMount() { 
    // // NOTE: ensures the sanity of our local storage with recursive method
    this.props.ensureAllFieldsPresent({
      projectUuid: this.props.params.projectUuid
    });
  }

Enter fullscreen mode Exit fullscreen mode

So when the action was called we would run this function, only passing the projectUuid to identify which project in local storage we wanted to check .

Whats happening in the reducer?

reducers/local-storage-settings.js


function ensureAllFieldsPresent(state, { projectUuid }) {
  let newState = JSON.parse(JSON.stringify(state));
  let fixedProjectState = recursiveCheck(newState[projectUuid]);
  newState[projectUuid] = fixedProjectState;
  localStorage.setItem('projectSettings', JSON.stringify(newState));
  return newState;
}


Enter fullscreen mode Exit fullscreen mode

So for each project we will run our recursive check, set local storage settings to the new 'fixed' state , and than lastly return the newState to our redux Store.

function recursiveCheck(state, defaultSettings = DEFAULT_SETTINGS) {
  let newState = JSON.parse(JSON.stringify(state));

  for (let k in defaultSettings) {
    if (!newState[k]) {
      // NOTE: 'in the event that a value is default set to TRUE'
      if (newState[k] !== false) {
        newState[k] = defaultSettings[k];
      }
    } else if (typeof defaultSettings[k] == 'object') {
      recursiveCheck(newState[k], defaultSettings[k]);
    }
  }
  return newState;
}

Enter fullscreen mode Exit fullscreen mode

Whats GOING ON ??!


function recursiveCheck(state, defaultSettings = DEFAULT_SETTINGS) {...}

Enter fullscreen mode Exit fullscreen mode

because we will be calling this function again (from within the function), we set a defaultSettings default so that when the method is called initially the first state object we use is the DEFAULT_SETTINGS object , which has the updated values we will be using to correct the users corrupt local storage object.


let newState = JSON.parse(JSON.stringify(state));

Enter fullscreen mode Exit fullscreen mode

since redux's state object is readonly you cannot mutate the object and return it , your must make a copy , make modifications and than return a new state.

were using JSON.parse(JSON.stringify(state) to make a deep copy of the object and get all those fields. (IF YOU KNOW A FASTER, BETTER WAY , COMMENT BELOW !)


  for (let k in defaultSettings) {
    if (!newState[k]) {
      // NOTE: 'in the event that a value is default set to TRUE'
      if (newState[k] !== false) {
        newState[k] = defaultSettings[k];
      }
    }

}
Enter fullscreen mode Exit fullscreen mode

the for loop : this will looop through our DEFAULT_SETTINGS Object and check (compare our default object to the users state) to see if we made any updates to any fields , but notice that it will only check the top level of the object, we want to go deeper and check the rest of the fields that are nested inside.

if (!newState[k]){...}
Enter fullscreen mode Exit fullscreen mode

So if the users local storage setting does not have a value in their local state , we want to create one for them (this covers null and undefined)

this part took me like a day to figure out


if (newState[k] !== false) {
        newState[k] = defaultSettings[k];
}

Enter fullscreen mode Exit fullscreen mode

so if the users state has a boolean false in their store and they are on the view in which the main component is mounted and this method is run, which is definitely possible, than what will happen is that this method will run and reset their store to DEFAULT_SETTINGS even though they they just clicked to set whatever option to false.

in this instance, when a user clicked to close something, it was not remembering that they closed it , and when we would navigate away and come back it was open again.

so in addition to checking !newState[k] i wrote another conditional inside (i know i could have done it in 1 line) to check to make sure that the newState[k] is initially not false. and if its not initially false we will replace the object.

The recursive.ness

So what happens when we run into an object in this loop?

We want to go deeper in to this object, and run the same key checking.


else if (typeof defaultSettings[k] == 'object') {
      recursiveCheck(newState[k], defaultSettings[k]);
    }

Enter fullscreen mode Exit fullscreen mode

So now when the loop gets to a value that is an object, this means we can go deeper , and check the keys inside of this object.

remember at the beginning, the initial state we passed (as the second argument) was DEFAULT_SETTINGS

function recursiveCheck(state, defaultSettings = DEFAULT_SETTINGS) {...}

Enter fullscreen mode Exit fullscreen mode

So this time, we will pass in the default[k] part of the state into the method as the second argument , and newState[k] as the first argument, this will ensure that the correct part of the users state object, and the correct part of the DEFAULT_SETTINGS is checked against each other.

NICE

now at the end of this we have fully checked every corner of this users Local storage settings, and now hopefully wont run into any bugs when we add new settings to the app.

Thanks so much for reading!

Top comments (5)

Collapse
 
markerikson profile image
Mark Erikson

I'm having a bit of trouble following exactly what this approach does, but I'm seeing a couple things that look like a bad idea.

First, re-running persistence logic in a component seems awkward at best, and doing so in componentWillMount is definitely a bad idea (if only because that lifecycle method is now deprecated).

Second, a reducer definitely shouldn't be interacting with local storage. That makes it no longer a pure function, and while the code might work, it's distinctly a bad practice.

Finally, this whole recursion aspect also seems overly complex. One possible approach might be to load the data as its own distinct action, and have appropriate reducers be responsible for filling in defaults as needed.

There's dozens of existing Redux addons for persisting your state - I'd suggest evaluating some of those, and using one of the existing solutions instead of something homegrown.

Collapse
 
nodefiend profile image
chowderhead • Edited

thank you so much again for your suggestions, the reason why i wrote this article was because this was actually a pretty difficult implementation for me to understand, so i wanted to solidify my understanding of what was going on by writing an article, im still a junior dev training

I also wanted to also maybe help out the community if i could with my experience.

Collapse
 
nodefiend profile image
chowderhead

Wow!

thank you so much for the suggestions, we are actually in the process of refactoring our actions and stores to a new version, and i can fit your suggestions into the new version of the reducer

  • we will definitely be taking it out of component did mount.
  • using an external library to handle local storage sounds like a good idea, especially if its a library tailored to redux.
  • thanks for your suggestion about removing local storage from the reducer , i wouldnt of known another way.
  • my senior engineer actually suggested that this be built this way, so i will bring up these suggestions when we refactor

  • i dont quite understand what you mean by loading the data as its own distinct action?

  • it is its own action , that happens once at the root component . could you maybe show me a code example of what you mean?!

thanks so much for reading ! :D really appreciate your time

Collapse
 
markerikson profile image
Mark Erikson

As a simplified example, you could have a higher-order reducer that looks for a "PERSISTED_DATA_LOADED" action and returns that:

function loadPersistedData(wrappedReducer) {
    return function persistedDataReducer(state, action) {
        if(action.type === "PERSISTED_DATA_LOADED") {
            return action.data;
        }

        return wrappedReducer(state, action);
    }
}

You could also have each individual slice reducer look for "PERSISTED_DATA_LOADED" and separately respond by pulling the bit of data it cares about out of action.data or action.payload or whatever you want to call it, and return that.

As for setup, typically persistence handling is done when you're creating the store on app startup, either by grabbing the persisted data first and passing it to createStore(), or dispatching some kind of "load the data" action right after you create it:

const preloadedState = JSON.parse(localStorage.get("persistedReduxState"));
const store = createStore(rootReducer, preloadedState);

// or
const store = createStore(rootReducer);

const preloadedState = JSON.parse(localStorage.get("persistedReduxState"));

if(preloadedState) {
     store.dispatch({type : "PERSISTED_STATE_LOADED", data : preloadedState});
}

But, as I said in my first reply, there's many existing libs that do this kind of stuff for you automatically once you set them up.

Thread Thread
 
nodefiend profile image
chowderhead

whoa! this is genius!

thank you so much for taking time to explain this to me !

its much clearer seeing the code for it.

Didnt even cross my mind to have a higher order reducer , what a great idea !

Will suggest a change to our team !

But first, I will look into the other library options for this first of course!

thanks again so much for your time