Vuex is the ideal tool for managing large and complex state in a Vue.js app. But when the code base gets huge, it might start to feel chaotic stitching your Vue components together with basic Vuex without using its additional features such as helpers and modules.
This tutorial will introduce these useful tools that can simplify your Vuex setup. Let’s look at how to apply them to an existing Vuex-driven app.
The sample app
We have the code uploaded to Github: https://github.com/Code-Pop/Vuex_Fundamentals.git
You can clone it to your local:
git clone https://github.com/Code-Pop/Vuex_Fundamentals.git
cd Vuex_Fundamentals
The L5-end branch is what we’re starting with here.
git checkout L5-end
This repo is originally from Vue Mastery’s Vuex Fundamentals course. The last lesson of that course is Lesson 5, and we’re picking up from there. That’s why the branch we’re starting with is L5-end. (If you can’t wait to see the final code with all the refactoring done, you can check out the refactor branch and take a look.)
Before we get into the code, let’s run the app first and get ourselves familiar with what it does.
First run npm install
to install all the dependencies:
npm install
Then we can start the server:
npm run serve
Go to localhost:8080 in a browser, and you can play around with the sample app.
The app has three pages:
- Events: a page that shows a list of events (like a simple Meetup.com)
- About: a page that shows some text describing the app
- Create Event: a page with a form to create a new event
Now, let’s take a look at the code. Our main components are hosted inside the src/views folder.
The focus of the tutorial is on the relationship between the Vue components and the Vuex store. Not all of our components are connected to Vuex, only these three components are:
- /src/views/EventCreate.vue
- /src/views/EventDetails.vue
- /src/views/EventList.vue
Here’s an illustration of how these Vue components are currently connected to the Vuex store:
As you can see, the Vuex store has three state items: user
, event
, and events
. And the components are consuming all three of them.
These three components are also dispatching various actions to the Vuex store, and here’s an illustration of that:
We have other components in the code base, but these three components are the only ones linking to the Vuex store, so they’ll be our focus in this tutorial.
Map Helpers
Map helper functions offer simpler ways for our components to connect to the Vuex store. Currently, we are getting the states and dispatching to the store all through this.$store
. Instead of this.$store.state.event
, using a map helper would allow us to simply write this.event
.
There are four map helpers from Vuex’s API.
mapState
mapGetters
mapActions
mapMutations
For our app, we’ll be using mapState
and mapActions
.
mapState
First, we’ll use mapState
to simplify the way we’re accessing user
and event
from the Vuex store.
We have three components to change, let’s start with EventCreate
.
First, import mapState
:
📃 /src/views/EventCreate.vue
<script>
import { v4 as uuidv4 } from 'uuid'
import { mapState } from 'vuex' // ADD
...
Then we’ll use mapState
to add a computed property user
to the component:
📃 /src/views/EventCreate.vue
computed: {
...mapState(['user'])
},
(the three-dot syntax is an ES6 syntax called the spread operator)
In the above code, the mapState
function will return an object that contains a user
method, and the spread operator (the three dots) will help us to put this user
method in the computed
object. As a result, user
becomes a computed
property for the component.
Now we can use this.user
in our component instead of this.$store.state.user
:
📃 /src/views/EventCreate.vue
onSubmit() {
const event = {
...this.event,
id: uuidv4(),
organizer: this.user // CHANGE
}
...
The mapState
function has added a computed property called user
that is “tracking” the user
state from our store. This is functionally the same as using this.$store.state.user
directly.
For the EventDetails component, we’ll remove the existing computed property event
, and create the same computed property using the mapState
function:
📃 /src/views/EventDetails.vue
<script>
import { mapState } from 'vuex' // ADD
...
computed: {
/* REMOVE
event() {
return this.$store.state.event
}
*/
...mapState(['event']) // ADD
}
And for the EventList
component, we’ll do the exact same thing again:
📃 /src/views/EventList.vue
<script>
import EventCard from '@/components/EventCard.vue'
import { mapState } from 'vuex' // ADD
export default {
...
computed: {
/* REMOVE
event() {
return this.$store.state.event
}
*/
...mapState(['events']) // ADD
}
If we need to map to multiple state items from the store, we can just add it to the array argument:
📃 /src/views/EventList.vue
computed: {
...mapState(['events', 'user']) // CHANGE
}
With user
added as a new computed property, we can render it in the template like this:
📃 /src/views/EventList.vue
<template>
<h1>Events for {{ user }}</h1>
...
By using the mapState
, we are now able to access the states just like regular computed
properties.
mapActions
Although we’ve improved the way we’re accessing the states, the components are still relying on this.$store.dispatch
for dispatching actions to the store. Instead of writing this.$store.dispatch('createEvent', event)
, we want to be able to just write this.createEvent(event)
.
So, we’ll use the mapAction
helper next.
Starting with the EventCreate component:
📃 /src/views/EventCreate.vue
<script>
import { mapState, mapActions } from 'vuex' // CHANGE
...
methods: {
...mapActions(['createEvent']), // ADD
onSubmit() {
const event = {
The mapActions
function will inject a createEvent
method into the component, which we can use to dispatch the action.
So now we can just call this.createEvent
directly instead of this.$store.dispatch
:
onSubmit() {
const event = {
...
}
// this.$store.dispatch('createEvent', event) REMOVE
this.createEvent(event) // ADD
.then(() => {
...
Let’s apply the same changes to the other two components.
📃 /src/views/EventDetails.vue
<script>
import { mapState, mapActions } from 'vuex' // CHANGE
...
export default {
props: ['id'],
created() {
// this.$store.dispatch('fetchEvent', this.id) REMOVE
this.fetchEvent(this.id) // CHANGE
.catch(error => {
...
})
},
computed: {
...mapState(['event'])
},
// ADD
methods: {
...mapActions(['fetchEvent'])
}
}
📃 /src/views/EventList.vue
import { mapState, mapActions } from 'vuex' // CHANGE
...
export default {
created() {
this.fetchEvents() // CHANGE
.catch(error => {
...
})
},
computed: {
...mapState(['events', 'user'])
},
// ADD
methods: {
...mapActions(['fetchEvents'])
}
}
By using mapState
and mapActions
, our components are now much cleaner.
Now, let’s direct our attention to the Vuex code.
Modules
Currently, all the Vuex code in this app is located in the same file, and that’s fine for a simple app. But as our app grows, the code is destined to be huge and complex, so this one-file setup is not an optimal way of managing our store. As a solution, we can break it up into organized chucks with a feature called modules.
Our current Vuex code can be refactored into two separate standalone modules: user
and event
. The user
module will contain all the Vuex code related to the user
state, and the event
module will contain all the Vuex code related to event
and events
.
We’ll work on extracting the code for the user
module first. Then, we’ll move on to the event
module.
Let’s create a new modules folder inside /src/store, where we’ll put all of the Vuex modules.
User module
Now, let’s begin with the user
module.
Create a file called user.js inside the module folder. This file will hold all of the user-related Vuex code. The only user-related code in our Vuex store is just the user
state. So, let’s extract the user
state to the new file:
📃 /src/store/modules/user.js
export default {
state: {
user: 'Adam Jahr'
}
}
A Vuex module has a similar structure as the main Vuex store. For example, we have a state
property here just like the code in store/index.js. But since there aren’t any mutations or actions related to user
, we are not using the actions
and mutations
properties in this module.
To make it look more like a real-life module, let’s expand the user state into an object with more user-related info:
📃 /src/store/modules/user.js
export default {
state: {
user: {
id: 'abc123', // ADD
name: 'Adam Jahr'
}
}
}
Now we have a module called user
, which is also the name of the state that it contains. To avoid confusion down the road, let’s rename the user
state to userInfo
.
📃 /src/store/modules/user.js
export default {
state: {
userInfo: { // RENAME
id: 'abc123',
name: 'Adam Jahr'
}
}
}
Back in store/index.js, we have to import the user
module and “plug” it into the store with the modules
property:
📃 /src/store/modules/user.js
import { createStore } from 'vuex'
import EventService from '@/services/EventService.js'
import user from './modules/user.js' // ADD
export default createStore({
state: {
// user: 'Adam Jahr', REMOVE
events: [],
event: {}
},
mutations: {
...
},
actions: {
...
},
modules: { user } // CHANGE
})
Since now that the user data is living inside a module, we have to modify our component code accordingly. Specifically, we need to prefix the state with the proper module name.
In the EventList
component, change user
to user.userInfo.name
:
📃 /src/views/EventList.vue
<template>
<h1>Events for {{ user.userInfo.name }}</h1>
<div class="events">
...
In the above code, user
is referring to the module, not the state. Since we’ve renamed it, userInfo
is now the state, and name
is a property inside the state object.
We don’t have to change the way we’re mapping to the store with mapState
because the module name is user
, which is the same name we are already using with mapState
.
And finally in the EventCreate
component, we need to change this.user
to this.user.userInfo.name
:
📃 /src/views/EventCreate.vue
const event = {
...this.event,
id: uuidv4(),
organizer: this.user.userInfo.name // CHANGE
}
And that’s it with the user
module. Next, we’ll work on the event
module.
Event module
First, create an event.js
file inside the store/modules folder. Then move all the event-related state
, mutations
, and actions
over to the new file.
📃 /src/store/modules/event.js
import EventService from '@/services/EventService.js'
export default {
state: {
events: [],
event: {}
},
mutations: {
ADD_EVENT(state, event) {
state.events.push(event)
},
SET_EVENT(state, event) {
state.event = event
},
SET_EVENTS(state, events) {
state.events = events
}
},
actions: {
createEvent({ commit }, event) {
return EventService.postEvent(event)
.then(() => {
commit('ADD_EVENT', event)
})
.catch(error => {
throw(error)
})
},
fetchEvents({ commit }) {
return EventService.getEvents()
.then(response => {
commit('SET_EVENTS', response.data)
})
.catch(error => {
throw(error)
})
},
fetchEvent({ commit, state }, id) {
const existingEvent = state.events.find(event => event.id === id)
if (existingEvent) {
commit('SET_EVENT', existingEvent)
} else {
return EventService.getEvent(id)
.then(response => {
commit('SET_EVENT', response.data)
})
.catch(error => {
throw(error)
})
}
}
}
}
(that’s basically the entire object literal from store/index.js, excluding the modules
property)
Let’s also rename the event state to currentEvent
so that it doesn’t have the same name as the event module:
📃 /src/store/modules/event.js
import EventService from '@/services/EventService.js'
export default {
state: {
events: [],
currentEvent: {} // RENAME
},
mutations: {
ADD_EVENT(state, event) {
state.events.push(event)
},
SET_EVENT(state, event) {
state.currentEvent = event // RENAME
},
...
Now, let’s head back into store/index.js. Just like what we did with the user
module, we have to import the event
module and “plug” it into the store:
📃 /src/store.index.js
import { createStore } from 'vuex'
// import EventService from '@/services/EventService.js' REMOVE
import user from './modules/user.js'
import event from './modules/event.js' // ADD
export default createStore({
modules: {
user,
event // ADD
}
})
With these changes in our Vuex code, our components also have to be updated to use the event
module.
Since now the events are living inside the event
module, we have to use the name event
instead of events
when we’re mapping with mapState
.
📃 /src/views/EventList.vue
computed: {
...mapState(['user', 'event'])
(that’s basically just removing the s
from events
)
And then we’ll prefix the events
in the template with the module name, event
.
📃 /src/views/EventList.vue
<template>
...
<div class="events">
<EventCard v-for="event in event.events" :key="event.id" :event="event" />
...
So once again, the event
in [event.events](http://event.events)
is the module name, and events
is one of the state items inside the event
module.
We’ll make similar changes in the EventDetails component:
📃 /src/views/EventDetails.vue
<template>
<div v-if="event.currentEvent">
<h1>{{ event.currentEvent.title }}</h1>
<p>
{{ event.currentEvent.time }} on {{ event.currentEvent.date }} @
{{ event.currentEvent.location }}
</p>
<p>{{ event.currentEvent.description }}</p>
</div>
</template>
There’s no need to change how we’re mapping with mapState
in this component because we are already mapping to event
.
Now the app should work just like before, but the event
data and the user
data are living in two separate modules.
Namespacing
You might have noticed that we didn’t change the way we’re dispatching the actions, and the code still works.
When an action is dispatched, the modules with the same action will get the chance to handle it, so we don’t have to specify which module we are dispatching to. Vuex is designed this way so that multiple modules can potentially handle the same action name.
But if we do want to make it clear which specific module we are dispatching to, we can use namespacing with our Vuex modules.
In the modules, add a namespaced
property and set it to true
.
📃 /src/store/modules/event.js
export default {
namespaced: true,
state: {
events: [],
currentEvent: {
...
📃 /src/store/modules/user.js
export default {
namespaced: true,
state: {
userInfo: {
...
Back in the components, add the right module name to mapActions
:
📃 /src/views/EventCreate.vue
methods: {
...mapActions('event', ['createEvent']),
onSubmit() {
📃 /src/views/EventDetails.vue
methods: {
...mapActions('event', ['fetchEvent'])
}
📃 /src/views/EventList.vue
methods: {
...mapActions('event', ['fetchEvents'])
}
And now, all of our dispatched actions are properly addressed to the right modules.
Conclusion
We’ve gone through two of the most useful refactoring tools in Vuex, map helpers and modules. As your app gets bigger, these tools are essential to keeping your code clean.
For more useful and in-depth content on Vuex, please check out Vue Mastery’s Mastering Vuex course.