Refactoring Vuex with Map Helpers and Modules

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:

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F1.1618933756226.jpg?alt=media&token=ce189e93-5736-4fb1-9ce3-b0266ea86792

  • 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:

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F2.1618933756227.jpg?alt=media&token=4856ec42-1b19-4b71-a114-e1e0193af732

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:

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F3.1618933763358.jpg?alt=media&token=608a45ce-c1ef-40f1-a942-225aff6e8a77

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>
  ...

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F4.1618933763359.jpg?alt=media&token=1ac9dd8e-d724-4345-b5ef-092f61de7646

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 modulesproperty:

📃 /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.

Download the cheatsheets

Our Vue essentials, Vue 3, and Nuxt.Js cheat sheets save you time and energy by giving you essential syntax at your fingertips.