Go Back to Pagination

Vuex Better Practice: Dynamic Modules

A large Vuex Store can get pretty unwieldy pretty fast. It is a good idea to split your Vuex Store into different modules when you are building any kind of large-ish application. You can seperate them by resource (e.g. products) or by concern (e.g. logging).

Example

This example is taken from the Vue Enterprise Boilerplate.

First you need to prepare your store/index.js

import Vue from 'vue';
import Vuex from 'vuex';

import modules from './modules';

Vue.use(Vuex);

const store = new Vuex.Store({
  modules
});

export default store;

As you can see, the index.js is pretty baren. We basically only register the store with Vue and import ./modules/index.jsand use its default export as our modules parameter to the store.

Let's look into the ./modules/index.js because that is where the real fun begins.


import camelCase from 'lodash/camelCase'

const modulesCache = {}
const storeData = { modules: {} }

;(function updateModules() {
  const requireModule = require.context(
    '.',
    true,
    /^((?!index|\.unit\.).)*\.js$/
  )

  requireModule.keys().forEach((fileName) => {
    const moduleDefinition =
      requireModule(fileName).default || requireModule(fileName)
    if (modulesCache[fileName] === moduleDefinition) return
    modulesCache[fileName] = moduleDefinition

    const modulePath = fileName
      .replace(/^\.\//, '')
      .replace(/\.\w+$/, '')
      .split(/\//)
      .map(camelCase)

    const { modules } = getNamespace(storeData, modulePath)

    modules[modulePath.pop()] = {
      namespaced: true,
      ...moduleDefinition,
    }
  })

  if (module.hot) {
    module.hot.accept(requireModule.id, () => {
      updateModules()
      require('../store').default.hotUpdate({ modules: storeData.modules })
    })
  }
})()


function getNamespace(subtree, path) {
  if (path.length === 1) return subtree

  const namespace = path.shift()
  subtree.modules[namespace] = {
    modules: {},
    namespaced: true,
    ...subtree.modules[namespace],
  }
  return getNamespace(subtree.modules[namespace], path)
}

export default storeData.modules

So that's a lot, lets go through it bit by bit. First, we import a function from lodash which lets us easily convert file names to camelcase.

Then we declare our module store for caching of modules and our store data which we'll export in the end. We use an IIFE to define our updateModules method. The first step in this method is the require.context call.

  const requireModule = require.context(
    '.',
    true,
    /^((?!index|\.unit\.).)*\.js$/
  )

This is a webpack function which requires a whole bunch of modules. In this case:

  • from the current Folder (.)
  • we are including subfolders (true)
  • we use any js file that is not the current index and not a unit test

If you then call something like requireModule(name)it'll actually require the module (at compile time). It also has a keys function property, which returns all names in an array.

requireModule.keys().forEach((fileName) => { ... })

We iterate over each name and do a couple of things. First we check whether or not we need to call '.default' (i.e. whether or not it was written using ES6 module syntax or node module syntax (export default vs module.exports)).

const moduleDefinition =
    requireModule(fileName).default || requireModule(fileName)

Then we check if the required module is already in the cache. If it is we dont need to do anything, so we return. If it isn't we update the cache and do the rest of the function.

 if (modulesCache[fileName] === moduleDefinition) return
    modulesCache[fileName] = moduleDefinition

We clean up the module path by removing starten ./ and trailing . and splitting it by directory, then we camelCase it. So the result is a camelcased array of directories and the actual file name.

const modulePath = fileName
      .replace(/^\.\//, '')
      .replace(/\.\w+$/, '')
      .split(/\//)
      .map(camelCase)

Then we recursivley build our namespace tree using the getNamespacefunction. So the result might look something like this:

{
    modules: {
        documents: {
            modules: {
                mail: {
                  state: {...},
                  actions: {...}
                  mutations: {...}
                }
                namespaced:true,

            },
            state: {...},
            actions: {...},
            mutations: {...}
        }
        namespaced: true
    }
}

And then we add our actual module to it. Module definition most likely contains at least a state, actions and mutation.

    modules[modulePath.pop()] = {
        namespaced: true,
        ...moduleDefinition,
    }

Note that this modules references goes to stateData which is defined outside of the loop.

Last but not least we hot-load changes if hot-loading is enabled (in dev environments usually). This is a webpack feature.

if (module.hot) {
    module.hot.accept(requireModule.id, () => {
      updateModules()
      require('../store').default.hotUpdate({ modules: storeData.modules })
    })
  }

Phew! That was a lot of work. But from now on we can easily declare modules and keep our state and actions seperated. All we have to do is create a new file and, if we want to, subdirectories and it will get automatically registered. Great!