Search code examples
javascriptvuexextends

How to further extend state of an extended Javascript Class Module


I am in the process of lessening the bulk of a Vuex store module that has grown too big and overly complex. Our state management system is built with Vuex using JS classes where we have a base module that is extended to multiple other more specific modules. I am currently refactoring one of those more ‘specific’ modules - this module handles the state of a couple views containing a handful of components. I am attempting to extend this module the same as all of our other modules and use spread syntax to bring in the existing state properties…

export default class ChildModule extends BaseModule {
  constructor() {
    super()
    this.state = {
       ...this.state
    }

    this.getters = {
       ...this.getters
    }
  }

    // ** the same syntax for actions and mutations 
    
    // logging rootState in an action inside GrandchildModule correct values from ChildModule
}

the only difference in the working module vs the broken module is the class that’s being extended…

export default class GrandchildModule extends ChildModule {...

for some reason that I haven’t figured out yet the GrandchildModule is not inheriting the state from the ChildModule properties that require fetching data from the api - properties that do not require api access are behaving correctly - I have been unsuccessful using getters in the GrandchildModule which I expected to be reactive to fetched data?

I’m hoping someone has some insight on how I can make this work or tell me what i’m doing wrong

update: found this on reactivity that i'm currently investigating - not 100% that this is my issue but it could be?

example structure - stackblitz with js classes

updated stackblitz


Solution

  • The first problem with the stackblitz you shared is that in each of the classes (except BaseClass) you spread state in constructor. And state is not defined. You probably meant this.state:

    export default class ChildClass extends BaseClass {
      constructor() {
        super();
    
        this.state = {
          ...this.state,
          name: 'ChildClass',
        };
      }
    }
    

    Secondly, in index.js you're attempting to display GreatGrandchild.id which, again, doesn't exist. GreatGrandchild is a JavaScript class declaration (which, technically, is a function with a single property: the name - the actual name of the class), so it doesn't have an id. Not even its instances (in your case, timmy) do not have an id. But they do have a state.id.

    For a better understanding of how JavaScript classes work, I recommend MDN.

    If you want to reference the inherited state.id (from BaseClass > ChildClass > GrandchildClass > GreatGrandchild), you could use:

    const timmy = new GreatGrandchild();
    console.log(timmy.state.id); // output: 0
    

    See it working here.


    From the example you posted it's unclear how your code is related to vuex. Or vue, for that matter. You might want to provide a minimal runnable example with what you have.

    However, on general principles, I would not recommend extending Vuex stores. Once a store has been instantiated, new members added to its state, actions, mutations or getters will not be reactive. If you're determined to take that route, this question might help.

    What could work for your case is a BaseClass which doesn't actually create a store, but holds the common store props and then have different store classes which actually create stores after merging the base class's props with their own. Something along these lines:

    class BaseStore {
      constructor() {
        // place here what you want defined on all stores
        this.state = {
          // ...
        };
        this.actions = {
          // ...
        };
        this.getters = {
          // ...
        };
        this.mutations = {
          // ...
        };
      }
    }
    
    class SomeStore extends BaseStore {
      constructor({ state = {}, mutations = {}, actions = {}, getters = {} }) {
        super();
        return new Vuex.Store({
          state: {
            ...this.state,
            ...state,
          },
          actions: {
            ...this.actions,
            ...actions,
          },
          getters: {
            ...this.getters,
            ...getters,
          },
          mutations: {
            ...this.mutations,
            ...mutations,
          },
        });
      }
    }
    
    class SomeOtherStore extends BaseStore {
      constructor({ state = {}, mutations = {}, actions = {}, getters = {} }) {
        super();
        return new Vuex.Store({
          state: {
            ...this.state,
            ...state,
          },
          actions: {
            ...this.actions,
            ...actions,
          },
          getters: {
            ...this.getters,
            ...getters,
          },
          mutations: {
            ...this.mutations,
            ...mutations,
          },
        });
      }
    }
    
    // declare or import state, actions, getters, mutations, otherState, otherActions, otherMutations and otherGetters
    
    const myFirstStore = new SomeStore({
      state,
      actions,
      getters,
      mutations
    });
    
    const myOtherStore = new SomeOtherStore({
      state: otherState,
      actions: otherActions,
      mutations: otherMutations,
      getters: otherGetters
    })
    

    Although the above pattern is possible, I'm skeptical about its usefulness in a real world scenario. I'm far from recommending it. The main reason is because classes are useful when you want to handle multiple instances of the same class. It's difficult to imagine a scenario where having multiple instances of the same store would be useful.


    Note: I've used Vuex 3 instantiation method, because I guessed you're using Vue2 + Vuex 3. If you use Vue 3 + Vuex 4, you should replace new Vuex.Store() with createStore()


    Another note: the above example could be written without using classes. I'd argue it's a bit cleaner:

    const defaultStoreProps = {
      state: {
        // common state
      },
      actions: {
        // common actions
      },
      // etc...
    }
    
    const myFirstStore = new Vuex.Store({
       state: {
         ...defaultStoreProps.state,
         // other state
       },
       actions: {
         ...defaultStoreProps.actions,
         // other actions
       },
       // etc...
    })
    
    const mySecondStore = new Vuex.Store({
      state: {
        ...defaultStoreProps.state,
        // other state
      },
      actions: {
        ...defaultStoreProps.actions,
        // other actions
      },
      // etc...
    })