Instance-Aware Vuex Modules - Part 1
Vuex is a great state-management tool provided by the Vue core team. It's especially useful in server-side rendered (SSR) applications as a mechanism to send the server-side state up to the client for the app hydration process. In this post, we'll look at some of the basics of Vuex and how we can use namespaced modules to isolate our logic into small, targeted modules.
Note: This is part 1 of a 3-part series on "Instance Aware Vuex Modules". In this post, we'll cover a basic introduction to Vuex, modules, namespaced modules, and the provided map*
helper functions.
Using a Basic Vuex Store
Wiring up a basic Vuex store only requires a few lines of code in our main application file:
import Vue from 'vue';
import Vuex from 'vuex';
// Tell Vue to use the Vuex plugin
Vue.use(Vuex);
// Create a Vuex store
const store = new Vuex.Store({
state: {
count: 0,
},
});
// Provide your store to your root Vue component
const app = new Vue({
el: '#app',
store,
});
And you're done! Now, every component in your application will have the Vuex store available via this.$store
. This injection is done via the registered Vuex plugin.
<template>
<h1>Count is {{count}}</h1>
</template>
<script>
export default {
computed: {
count() {
return this.$store.state.count;
},
},
}
</script>
Namespaced Vuex Modules
As our application grows, and we add more data to state
and corresponding mutations
and actions
to our store, we'll find that it starts to grow in size rather quickly. As your store file grows, sometimes some folks have recommended splitting state
/mutations
/actions
/getters
into their own files and combining them in a store/index.js
file. Personally, I am not a fan of this approach, as it adds a level of context-switching that I find slows down a developers ability to move quickly. For example, when you are writing a mutation
, you are directly mutating the state
object. If you have to reference the state
object to determine it's shape, I would prefer to scroll upwards within the same file then switch to a different file all together. Similar arguments can be made when writing actions
using mutations
, or getters
accessing state
I find a much cleaner approach to be to split your store into Vuex sub-modules focused on functionality instead of splitting by portions of the store. In an e-commerce application, this might consist of a handful of Vuex modules:
- An
auth
module for manging the user's authentication status - A
cart
module for managing the items in the user's cart - A
header
module for managing the state of the main header and navigation tree - ... and so on
By splitting our main Vuex module into a handful of smaller sub-modules, we can keep these sub-modules quite small and focused, and keep our state
/mutations
/actions
/getters
to a size that permits them to remain in a single file.
Here is how we would set up some modules in our Vuex store:
///// auth-store.js /////
export default {
namespaced: true,
state: {
loggedIn: false,
},
mutations: {
setLoggedIn(state, loggedIn) {
state.loggedIn = loggedIn;
},
},
}
///// cart-store.js /////
export default {
namespaced: true,
state: {
items: [],
},
mutations: {
addItem(state, item) {
state.items.push(item);
},
},
getters: {
itemCount(stat) {
return state.items.length;
},
},
}
///// app.js /////
import authStore from './auth-store';
import cartStore from './cart-store';
const store = new Vuex.Store({
state: { ... },
modules: {
auth: authStore,
cart: cartStore,
},
});
const app = new Vue({
el: '#app',
store,
});
Notice that each Vuex sub-module is the exact same shape as the root-module, except that we've added a namespaced: true
property. This is optional and defaults to false
, but I would always suggest you use it as it leads to your store being much easier to work with and debug - and it also avoids naming conflicts across different sub-modules.
Using namespaced modules, our store shape and usage starts to look a little different. Each module's state
is stored in an object off the root store matching the name of the module at registration time. Similarly, mutations
, actions
, and getters
are prefixed with the module name and a slash.
Here's how we would access those namespaced modules from a component:
export {
computed: {
isLoggedIn() {
return this.$store.state.auth.loggedIn;
},
itemCount() {
return this.$store.getters['cart/itemCount'];
},
},
methods: {
logout() {
this.$store.commit('auth/setLoggedIn', false)l
},
addCartItem(item) {
this.$store.commit('cart/addItem', item);
},
},
}
Vuex map* Helper Methods
As we start using more and more modules, and even potentially start nesting them more than one level deep, it gets tedious to continue to type out the full module namespace each time you want to work with a given module. Thankfully, Vuex provides some helper functions that make this much more concise in your components
Using these helpers, we can simplify the above component example to the following:
import { mapState, mapGetters, mapMutations } from 'vuex';
export {
computed: {
...mapState('auth', ['loggedIn']), // Exposes this.loggedIn
...mapGetters('cart', ['itemCount']), // Exposes this.itemCount
},
methods: {
...mapMutations('auth', {
logout: 'setLoggedIn', // Exposes this.logout()
})
...mapMutations('cart', {
addItem: 'addItem', // Exposes this.addItem()
})
},
}
This approach really starts to benefit your component code once you start using multiple mappers per store, as it reduces the boilerplate of adding a new computed property or method every time you need to access a new state
field, mutation
, etc.
I hope this gives a basic introduction to Vuex and it's use of namespaced modules. In part 2 of this series, we'll look into using these namespaced modules to provide dynamic vuex stores for use alongside dynamic routes in vue-router. Thanks for reading!