jrnl.etnbrd.com

Mixin composition in Vuex

by etnbrd
May 01, 2018

When using Vuex or Redux, I often wonder how to compose the different parts of the store. I imagine that by following the flux pattern, I will end up with only one object — as expected — along with a myriad of actions, mutations and getters. All of these will likely grow in complexity with the evolution of the application, and probably not get along together very well.

Imagine yourself onboarding an existing application, with countless number of actions and mutations, and an intricate state structure. You might have a hard time grasping and building a mental structure of the whole thing in order to modify it.

The Single Responsibility Principle

My major concern is how to ensure the single single responsibility principle. I want to write parts so that each one resolves a precise and unique task. Then compose them together into bigger entities to build my store. Moreover, these entities might share some behaviors, so reusability might be a nice bonus.

Vuex already provides modules to help decompose a store into smaller entities. These modules can be nested within each other, but they lack a composition for extension ; similar, to the way mixins extend classes. And that’s exactly what I want in order to ensure the single responsibility principle: enclosing the features of a vuex module into different parts.

With this purpose in mind, I tried to sketch out some patterns and helpers using Vuex. This article explains my work so far on extending store modules in Vuex.

It illustrates this work on a very simple note taking application. The excerpt here are simplified version, but you can find the code for this toy application, with the helpers, in its own repository.

Note module

The main component of this note-taking application represent — unsurprisingly — notes. In our application, a note is simply composed of a name and a message.

Let’s start with a module to store a note. A Vuex module encapsulates a state along with its specific actions, mutations and getters.

The note module allows to store a note, and provide a mutation to update it. I am used to keep the mutations private and to expose only actions, which explains the dumb update action that acts as a proxy for the mutation.

const note = {
  namespaced: true,
  state () {
    return {
      name,
      message
    }
  },
  actions: {
    update({ commit }, note) {
      commit('update', note);
    }
  },
  mutations: {
    update(state, note) {
      Object.assign(state, { ...note })
    }
  }
}

draft behavior

A super nice feature to add to this note application would be to be able to revert the edits made to a note since last saving. This implies to edit a draft version, while the store keeps the last saved version.

That is a pretty well defined extension for our note module. It would provide two actions: save and revert, and keep a state of the note, exempt of edition, in case the user wants to revert to it. I named behaviors the parts extending existing Vuex modules. Roughly, behaviors are to modules what mixins are to classes. So, this draft behavior will extend the note module above.

The code below is pretty straightforward. Except for the super nice use of the spread operator in the save mutations, to exclude a property on an object copy. This use of the spread operators makes it super easy to stay completely agnostic from the structure of the main module it extends.

export draft = {
  state(init) {
    return {
      __saved: cloneDeep(init)
    }
  },
  actions: {
    revert({ commit }) {
      commit('revert');
    },
    save({ commit }) {
      commit('save');
    }
  },
  mutations: {
    revert(state) {
      Object.assign(state, state.__saved)
    },
    save(state) {
      // Exclude __saved from the state to save
      const { __saved, ...draft } = state;
      Object.assign(state.__saved, { ...draft })
    },
  }
}

I am sure you noticed the init parameter passed to the state function. For the behavior to extend the state from the main module, it needs to introduce a __saved attribute reflecting the initial structure. The initial structure of the state is provided in this parameter by the storeComposer helper presented below.

storeComposer

We now have a note module, and a draft behavior ; the next step is to extend the former with the latter. The storeComposer helper takes a Vuex module — here the note module — along with a list of behaviors — here, only the draft behavior — and compose them into an extended Vuex module.

Vuex modules are plain Javascript objects ; this helper only merges the objects describing the module and the behaviors into one. It relies heavily on the spread operator and Object.assign. Because of this, it might not be that straightforward (maybe I 💛 ES6 too much). So I commented every steps as much as I could.

export default function(module, ...behaviors) {
  return {
    // Include everything from the main module (the namespaced attribute, for example)
    ...module,


    // This state function calls, and compose the resulting state of the functions of
    // all the behaviors, and finally overwrites it with the result from the main module.
    state() {
      const init = module.state();
      return Object.assign({},
        // Returns an array of instantiated state functions from the behaviors,
        // then spreads it into the arguments of Object.assign, to extend the object.
        ...behaviors.map(behavior => behavior.state && behavior.state(init)),
        init
      );
    },


    // Populate the three static attributes with the behaviors attributes,
    // overwritten by the main module
    ...['getters', 'actions', 'mutations'].reduce((composed, attr) => {

      // Accumulates the defined attributes into `composed`
      return {
        ...composed,
        [attr]: Object.assign({},
          ...behaviors.map(behavior => behavior[attr]),
          module[attr]
        )
      };
    }, {})
  };
}

With this helper, we can now build bigger store modules while keeping the complexity manageable.

You could then extend the note module with a check behavior, to allow them to be checked when you complete them.
Or a due date behavior.
Or an author behavior.
Or a …
Infinite possibilities.

Infinite possibilities

The behaviors listed above seem too simple to justify the encapsulation. Though, the granularity for the behaviors doesn’t have to be this small. Still, the ability to split store entities into smaller parts might help you design your store better, and ensure the single responsibility principle.

Accountability

In this example, the draft behavior only needs to know about the structure of its own state, and not that of the main module nor the others behaviors.

Keeping the concerns of the behavior separate should always be a top priority. However, it might not always be possible. The behaviors might get in conflict ; between themselves, or with the main module. Hence, there needs to be some accountability to make sure the composition goes well. It seems to me that the main module should bear this responsibility.

That’s the reason why I wrote the storeComposer helper this way: to allow the main module to take precedence over the behaviors. All of the complexity of the main module could be abstracted into different behaviors, to keep only the glue code between these behaviors.

Final thoughts

This is more of a proof-of-concept than a bullet-proof implementation.

Currently, the storeComposer simply overwrite behaviors actions, mutations and getters with the one from the main module. But storeComposer could allow the main module to access and call the different parts of the behaviors, so as to compose their execution, modify their parameters and so on…

Moreover, it only allows to extend modules by adding more state, actions, mutations and getters, similarly to the decorator pattern. It doesn’t allow to extend modules by modifying the execution of these actions and getters, or by creating computed state, similarly to the proxy pattern. It still lacks some features to become what we could call a Higher Order Module pattern ; comparable to the Higher Order Component pattern.

I would love to hear your feedbacks about your own difficulties using Vuex modules in large applications, and your own solutions.


Special thanks to David Mamane (Sotaan) and Marvin Mottet who helped me working on this.