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.js
and 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 getNamespace
function. 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!