Vuex Module Composition

February 10th 2020

State management can be a wonderful thing to incorporate into your application, but as your app grows it can become increasingly difficult to scale your store. Luckily, vuex (used in vue applications) allow you to organize your store/state in modular form so you can more easily scale your app. Today, we’ll explore both Vuex Modules to help us compartmentalize our state management.

Let’s start with the Vuex Module mode. As defined in the vuex docs:

Vuex allows us to divide our store into modules. Each module can contain its own state, mutations, actions, getters, and even nested modules

vuex.vuejs.org

Let’s take a look at what this might look like in a real world application. Say you need to manage a state called cart. Inside your cart you are going to keep track and manage: items (which is an array of products), total (which is a number) and customerId (unique identifier to customer on the backend). You might also want to manage other data in state, for instance, if a global navigation sidebar is toggled on or off. You could store both the navigation state and the cart state in a single state tree (one big object), but you are more than likely going to have specific actions and mutations that deal with their respective state object. Here’s what storing state in one state tree might look like:

const store = new Vuex.Store({
  state: {
    cart: {
      items: [],
      total: 0,
      customerId: null
    },
    navigation: {
      show: false
    }
  },
  mutations: {
    addToCart(state, payload){
       state.cart.items = [...state.cart.items, payload]
    }
    toggleNavigation (state) {
      state.navigation.show = !state.navigation.show
    }
  }
})

To use one of these actions we could commit a method such as:

store.commit('toggleNavigation')

// or commit an item

store.commit('addToCart', { name: 'Gogo Widget', id: 123, price: '12.50' })

As you can see by our example, the state for cart has no relationship with the state for navigation and does not need to know (or care to know) about the state of our navigation. These two stores can be broken up into their own modules so that as our code base grows (we add new mutations/actions/getters etc) we are composing our state in different modules/files. Here is what our composition might look like broken up.

const cart = {
  state: {
    items: [],
    total: 0,
    customerId: null
  },
  mutations: {
    addToCart(state, payload){
       state.cart.items = [...state.cart.items, payload]
    }
  }
}

const navigation: {
  state: {
    show: false
  },
  mutations: {
    toggleNavigation (state) {
      state.navigation.show = !state.navigation.show
    }
  }
}

const store = new Vuex.store({
  modules: {
    cart,
    navigation
  }
})

Now using this setup doesn’t change much as for calling our actions and mutations won’t change unless we add a namespace property to our module (which we’ll walk through in a bit). Accessing state in our components would still be the same:

<template>
  <div>
    <span>{{ total }}</span>
  </div>
</template>
<script>
import { mapState } from 'vuex'
export default {
  computed: {
    ...mapState({
      total: state => state.cart.total
    })
  }
}
</script>

Also, since the modules are not namespaced (at this point) we would dispatch actions to the global namespace:

By default, actions, mutations and getters inside modules are still registered under the global namespace – this allows multiple modules to react to the same mutation/action type.

vuex.vuejs.org
<template>
  <div>
    <span>{{ total }}</span>
    <button @click="addToCart">Add to Cart</button>
  </div>
</template>
<script>
import { mapState } from 'vuex'
export default {
  data: {
    return {
      product: { name: 'Gogo Widget', id: 123, price: '12.50' }
    }
  },
  computed: {
    ...mapState({
      total: state => state.cart.total
    })
  },
  methods: {
    addToCart(){
      this.$store.commit('addToCart', this.product)
    }
  }
}
</script>

Now if we want to decouple the modules even more we can add namespaced: true to our module definitions:

const cart = {
  namespaced: true,
  state: {
    items: [],
    total: 0,
    customerId: null
  },
  mutations: {
    addToCart(state, payload){
       state.cart.items = [...state.cart.items, payload]
    }
  }
}

const navigation: {
  namespaced: true,
  state: {
    show: false
  },
  mutations: {
    toggleNavigation (state) {
      state.navigation.show = !state.navigation.show
    }
  }
}

const store = new Vuex.store({
  modules: {
    cart,
    navigation
  }
})

This would not affect our components state mapping as the docs state:

<template>
  <div>
    <span>{{ total }}</span>
    <button @click="addToCart">Add to Cart</button>
  </div>
</template>
<script>
import { mapState } from 'vuex'
export default {
  data: {
    return {
      product: { name: 'Gogo Widget', id: 123, price: '12.50' }
    }
  },
  computed: {
    ...mapState({
      total: state => state.cart.total
    })
  },
  ...
}
</script>

module state is already nested and not affected by namespace option

vuex.vuejs.org

However, this would affect our method/action mapping in our components by adding a namespace to our commit and/or dispatch: this.$store.commit('cart/addToCart', this.product)

<template>
  <div>
    <span>{{ total }}</span>
    <button @click="addToCart">Add to Cart</button>
  </div>
</template>
<script>
import { mapState } from 'vuex'
export default {
  data: {
    return {
      product: { name: 'Gogo Widget', id: 123, price: '12.50' }
    }
  },
  computed: {
    ...mapState({
      total: state => state.cart.total
    })
  },
  methods: {
    addToCart(){
      this.$store.commit('cart/addToCart', this.product)
    }
  }
}
</script>

If you want to use the mapMutations helper function you are going to want to pass the namespace to the mapMutations function to help map your methods:

<template>
  <div>
    <span>{{ total }}</span>
    <button @click="add">Add to Cart</button>
  </div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
export default {
  data: {
    return {
      product: { name: 'Gogo Widget', id: 123, price: '12.50' }
    }
  },
  computed: {
    ...mapState({
      total: state => state.cart.total
    })
  },
  methods: {
    add(){
      this.addToCart(this.product)
    },
    ...mapMutations('cart', [
      'addToCart'
    ])
  }
}
</script>