Vue.js 3 Vuex Modules Tutorial

In this Vue tutorial we learn how to organize a store with Vuex modules.

We cover namespacing and file structure and go through the process of organizing a store step by step.

Lesson Project

This lesson is a continuation of the previous .

If you want to follow along with the examples in this lesson, you will need an app generated by the Vue CLI with the Vuex package installed .

The project should look similar to the following.

Example: project
project-name/
├── src/
|   ├── main.js
|   └── App.vue

The store is in the /src/main.js file. It uses a getter to track the state of a counter and an action that commits an increment mutation with a payload.

Example: src/main.js
import { createApp   } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'

const store = createStore({
  state() {
    return {
      counter: 0
    }
  },
  getters: {
    getCounter(state) { return state.counter }
  },
  mutations: {
    increment(state, payload) {
      state.counter = state.counter + payload
    }
  },
  actions: {
    increment(context, payload) {
      context.commit('increment', payload);
    }
  }
})

const app = createApp(App)

app.use(store)
app.mount('#app')

The root App uses mappers to track and increment the counter in the store.

Example: src/App.vue
<template>
  <p>Counter: {{ trackCounter }}</p>
  <button @click="incrementCounter(7)">Increment</button>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'

export default {
  methods: {
    ...mapActions({incrementCounter: 'increment'})
  },
  computed: {
    ...mapGetters({trackCounter: 'getCounter'})
  }
}
</script>

How to organize a store with modules

To keep our code more manageable, we can break up our store into multiple modules.

A module is just an object that can contain all the same options as a store.

To demonstrate, let’s move all our options from the store into a counterModule object.

Example: src/main.js
import { createApp   } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'

const counterModule = {
  state() {
    return {
      counter: 0
    }
  },
  getters: {
    getCounter(state) { return state.counter }
  },
  mutations: {
    increment(state, payload) {
      state.counter = state.counter + payload
    }
  },
  actions: {
    increment(context, payload) {
      context.commit('increment', payload);
    }
  }
}

const store = createStore({ })

const app = createApp(App)

app.use(store)
app.mount('#app')

We then tell Vuex that we want to include the module in the store by specifying it in the store’s modules option.

Example: src/main.js
import { createApp   } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'

const counterModule = {
  state() {
    return {
      counter: 0
    }
  },
  getters: {
    getCounter(state) { return state.counter }
  },
  mutations: {
    increment(state, payload) {
      state.counter = state.counter + payload
    }
  },
  actions: {
    increment(context, payload) {
      context.commit('increment', payload);
    }
  }
}

const store = createStore({
  modules: {
    counterModule
  }
})

const app = createApp(App)

app.use(store)
app.mount('#app')

If we take a look at the browser, everything will still work as expected. Importing a module is the same as defining the code in the store itself.

We’re using the ES6 syntax where we combine the key and value if they are the same. We can set the key to a different value and this may be a situation where we want to do so.

Example:
const store = createStore({
  modules: {
    counterMod: counterModule
  }
})

note By default, Vuex automatically creates a module for us called the root module. If we don’t manually set up any modules, everything in the store will be part of that root module.

How to namespace a module

To avoid naming clashes as our store grows, we can put a module into a unique namespace.

As an example, consider that we have two modules with a counter property.

Because Vuex merges both modules into the store, their names will clash. Vuex won’t know which counter’s state to change when we refer to it.

If we put each module into a namespace, we refer to the counter property through that namespace. That means Vuex will know which counter we are referring to.

To place a module into a namespace, we set the module’s namespaced option to true.

Example: src/main.js
import { createApp   } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'

const counterModule = {
  // namespace the module
  namespaced: true,
  state() {
    return {
      counter: 0
    }
  },
  getters: {
    getCounter(state) { return state.counter }
  },
  mutations: {
    increment(state, payload) {
      state.counter = state.counter + payload
    }
  },
  actions: {
    increment(context, payload) {
      context.commit('increment', payload);
    }
  }
}

const store = createStore({
  modules: {
    counterMod: counterModule
  }
})

const app = createApp(App)

app.use(store)
app.mount('#app')

The namespace will be the key we specified in the modules option of the store.

Now when we refer to things when we are outside of the module, we have to use a different syntax that includes the namespace.

Convenience Mapper Methods:

If we’re using the mapping convenience methods, we simply specify the namespace as a string in the first argument of the methods.

Example: src/App.vue
<template>
  <p>Counter: {{ getCounter }}</p>
  <button @click="increment(3)">Increment</button>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'

export default {
  methods: {
    ...mapActions('counterMod', ['increment'])
  },
  computed: {
    ...mapGetters('counterMod', ['getCounter'])
  }
}
</script>

Direct Getter Access:

When we directly access something in a module, we specify the namespace, a slash and then what we want to access.

Example: direct access namespace
'namespace/getter_or_action'

In the case of getters, we use the getters[] array and specify the namespace with the syntax above.

Example: src/App.vue
<template>
  <p>Counter: {{ trackCounter }}</p>
  <button @click="increment(3)">Increment</button>
</template>

<script>
import { mapActions } from 'vuex'

export default {
  methods: {
    ...mapActions('counterMod', ['increment'])
  },
  computed: {
    trackCounter() {
      // use getters as an array and specify
      // 'counterMod' namespace with a slash
      // before the getter method
      return this.$store.getters['counterMod/getCounter']
    }
  }
}
</script>

Direct Action Dispatching:

When we’re dispatching an action, we just specify the namespace syntax before the action name.

Example: src/App.vue
<template>
  <p>Counter: {{ trackCounter }}</p>
  <button @click="incrementCounter">Increment</button>
</template>

<script>
export default {
  methods: {
    incrementCounter() {
      // specify 'counterMod' namespace
      // before the action name
      this.$store.dispatch('counterMod/increment', 5)
    }
  },
  computed: {
    trackCounter() {
      // use getters as an array and specify
      // 'counterMod' namespace with a slash
      // before the getter method
      return this.$store.getters['counterMod/getCounter']
    }
  }
}
</script>

Vuex module file structure

The whole point of using modules is to split our code into smaller, more manageable sections. If we have everything in our /src/main.js file, it will quickly become very large.

Typically, a Vuex module file structure looks as follows.

Example: Module file structure
src/
|
├── assets				  // Project assets
├── components			  // Project components
|
├── store/
|   ├── module_name/
|   |   ├── actions.js    // Module actions
|   |   ├── getters.js    // Module getters
|   |   ├── mutations.js  // Module mutations
|   |   └── index.js      // Module state, namespace, export
|   |
|   ├── module_name/
|   |   ├── actions.js    // Module actions
|   |   ├── getters.js    // Module getters
|   |   ├── mutations.js  // Module mutations
|   |   └── index.js      // Module state, namespace, export
|   |
|   └── index.js 		  // Main object, modules, export
|
├── main.js				  // Main import, use store
└── App.vue				  // Root component

Let’s see how to separate our counter module in such a file structure.

1. The first step is to separate Vuex from the /src/main.js file.

Let’s start by creating /store/index.js and then cut the following from the /src/main.js file and paste it into the /store/index.js file.

  • The ‘vuex’ import statement
  • The counterModule object
  • The store object

Finally, we export the store as a default export .

Example: src/store/index.js
import { createStore } from 'vuex'

const counterModule = {
  namespaced: true,
  state() {
    return {
      counter: 0
    }
  },
  getters: {
    getCounter(state) { return state.counter }
  },
  mutations: {
    increment(state, payload) {
      state.counter = state.counter + payload
    }
  },
  actions: {
    increment(context, payload) {
      context.commit('increment', payload);
    }
  }
}

const store = createStore({
  modules: {
    counterMod: counterModule
  }
})

// export store to use in main.js
export default store

Once we’ve exported the store , we can import it into the /src/main.js file to use in the App.use method.

Example: src/main.js
import { createApp   } from 'vue'
import App from './App.vue'

// import counterModule
import store from './store/index'

const app = createApp(App)

// use imported store
app.use(store)
app.mount('#app')

If we run the app in the browser, everything should still work as expected.

2. The second step is to separate the module from the index.js file.

We will start by creating the /store/counter/ folder and the index.js file inside it.

Then, we cut the entire counterModule object from /store/index.js and put it inside /store/counter/index.js .

Finally, we export the counterModule as a default export .

Example: src/store/counter/index.js
const counterModule = {
  namespaced: true,
  state() {
    return {
      counter: 0
    }
  },
  getters: {
    getCounter(state) { return state.counter }
  },
  mutations: {
    increment(state, payload) {
      state.counter = state.counter + payload
    }
  },
  actions: {
    increment(context, payload) {
      context.commit('increment', payload);
    }
  }
}

export default counterModule

Once we’ve exported the counterModule, we can import it into the /src/store/index.js file to use it in the modules option.

Example: src/store/index.js
import { createStore } from 'vuex'

// import counterModule to
// use in createStore()
import counterModule from './counter/index'

const store = createStore({
  modules: {
    counterMod: counterModule
  }
})

// export store to use in main.js
export default store

We can test the app in the browser again to make sure everything still works as it should.

note If the module you’re working with isn’t too big, you can keep everything in the /store/module_name/index.js file.

With “too big” we mean anything larger than around 250 lines of code. Our counterModule is rather small so we could leave it as is, but for the demonstration we will break it up fully.

3. The last step is to break up getters, actions and mutations into separate files.

We will start by creating the following 3 files.

  • /store/counter/getters.js
  • /store/counter/actions.js
  • /store/counter/mutations.js

Then, we create an object in each file and cut their objects from the options in the store/counter/index.js file.

Finally, we export the object in each file as a default export .

Example: store/counter/getters.js
const counterGetters = {
  getCounter(state) { return state.counter }
}

export default counterGetters
Example: store/counter/mutations.js
const counterMutations = {
  increment(state, payload) {
    state.counter = state.counter + payload
  }
}

export default counterMutations
Example: store/counter/actions.js
const counterActions = {
  increment(context, payload) {
    context.commit('increment', payload);
  }
}

export default counterActions

Once they’re exported, we can import them into the /store/counter/index.js file and specify them as the objects in their respective options.

Example: store/counter/index.js
// import getters, actions & mutations
import counterGetters   from './getters'
import counterActions   from './actions'
import counterMutations from './mutations'

const counterModule = {
  namespaced: true,
  state() {
    return {
      counter: 0
    }
  },
  getters:   counterGetters,
  actions:   counterActions,
  mutations: counterMutations
}

export default counterModule

And that’s it, our module is completely broken up and each file in the chain is easy to work with and test.

We can do one final test in the browser to see that everything still works as it should.

Further Reading

For more information on the topics covered in this lesson, please see the relevant section below.