Search code examples
javascriptlocal-storagevuexweb-frontendvuex-persist

How to introduce changes to Vuex persisted state?


We are using persisted localstorage state in our application.

When adding new fields or values to the store module, they are not applied to existing users' states. It only works when clearing the storage.

How should one go about solving this issue?

One possible solution I have thought of is following:

  1. Create a prototype/helper object, which is used in the state initialization
  2. On app load, get the user's state object and compare key/value pairs to this original object
  3. Find which data is missing and add these to the user's state object

Is it a viable option or are there better ways of handling this issue?


Solution

  • Without getting much into implementation details you should merge the localStorage state with the existing vuex state like this:

    export const mutations: MutationTree<YourState> = {
       MERGE_USER_STATE(state, userState: Partial<YourState>) {
          state = {...state, ...userState};
       }
    }
    

    This will extend your existing state with a previous state cached inside the users localStorage. Existing keys will be overriden while new keys are preserved.

    This has a few drawbacks though:

    • nested objects are always overridden. So new keys inside objects get overriden
    • you loose track of what is stored in each users state which can become quite complicated to debug.

    A better way would be to use a versioning mechanism and handle the transformation yourself. That way you have knowledge about the schema stored inside the user's localstorage and can transform that schema into anything you want.

    Given a user with a localstorage state item containing:

    { 
      version: 1, 
      data: {
         favoriteAnimals: ["Dog", "Cat"]
      }
    }
    

    And given a vuex store that allows users to add/edit their favorite animals:

    export const state = () => {
      version: 1,
      favoriteAnimals: []
    }
    
    export const mutations = {
       ADD_ANIMAL(state, animal: string){
         state.favoriteAnimals.push(animal);
         localStorage.setItem('state', JSON.stringify({
          version: state.version,
          data: state
       })
    )
    
       },
       // more mutations to remove and whatnot
    }
    
    export const actions = {
       add(context, animal: string){
          commit("ADD_ANIMAL", animal);
       },
    
       load(context, userState: string){ // userState = localStorage state
          const {version, data} = JSON.parse(state);
          // for now we don't care about data. we assume data is an array of strings
         data.forEach(animal => commit("ADD_ANIMAL", animal);
       }
    }
    

    Now a few weeks in your team decides to change the list of favorite animals from a list of strings into a list of objects like this:

    [ { animal: "Dog", ranking: 5 } ]
    

    Obviously when the user visits with his old localstorage your app will break as the types are incompatible now.

    You can now edit your load action like this:

    load(context, userState: string){ // userState = localStorage state
         const {version, data} = JSON.parse(state);
         if (version === 1){ // old user detected.
            data.forEach(animal => {
                commit("ADD_ANIMAL", { name: animal, ranking: 0 });
            });
            return;
         }
         
         // default action when the version is up to date
         data.forEach(animal => commit("ADD_ANIMAL", animal);
    }
    

    At a certain scale this will grow into a bigger and bigger function although I think it's fine as it's easy to extend. You can always add analytics and track which version your users are actually on. This also has the added benefit that the state of each user gets updated automatically whenever they open the app as vuex will update the state accordingly after commiting to it's internal state.