Migrating A VueJS App To Vuex

One of the difficult things about getting started with Vuex is that it is not so much a library as it is a design pattern. It follows that implementing Vuex is not so much about using an API, as it is about structuring your code to comply with the pattern. If you’re new to Vuex, this will be daunting.

In this article, I’ll demonstrate how to get started migrating Vuex into an existing Vue.js project. I’ll show you how to identify the parts of your app’s state that belong in Vuex, and those that don’t, how to refactor your component functions into mutations, actions and so on, and finally we’ll discuss the benefits accrued.

If you’re not sure why you should use Vuex, I recommend you read this post first WTF Is Vuex: A Beginner’s Guide to Vue’s Application Data Store.

Case study: Vue.js Cinema

As a case study, we’ll migrate a demonstration app I made called Vue.js Cinema to Vuex. It’s a single-file component-based Vue app that is a good enough candidate for Vuex as it has a significant amount of application state.

If you want to learn how to build Vue.js Cinema from scratch, it’s part of my Ultimate Vue.js Developers course.

Remember that Vuex’s purpose is to manage application state. It follows that in order to migrate Vue.js Cinema to Vuex, we must identify its state. We’ll see the state in the code shortly, but it’s helpful to first infer it by simply observing what the app does i.e. it displays a list of movies and session times that can be filtered by changing the day, or toggling time and genre filters.

What state belongs in Vuex?

By using Vue Devtools, or just inspecting the code, we can see the data of the instance and each component:

Our objective is not to move all data to the Vuex store. Instead, we want to target data which:

  1. Changes throughout the lifecycle of the application (static data doesn’t need much management).
  2. Is shared by more than one instance/component.

We when say application state, this is what we’re talking about, and this is what we want to move into the store.

For example, look at this bank of check-filter components, which are custom checkboxes used to filter the movie list:

Each check-filter component has a checked data property, and also a title that is passed to it as a prop e.g. Before 6pm, After 6pm etc:

src/components/CheckFilter.vue

<template>...</template>
<script>
  export default {
    data() {
      return {
        checked: false
      }
    },
    props: [ 'title' ],
    ...
  }
</script>

The checked data property is text-book application state, as it changes throughout the lifecycle of the app, and other components will certainly need to react to its state.

The title prop, on the other hand, does not change, and does not affect any other component. It therefore does not need to be managed by Vuex.

Note: if you have any local state that’s important enough that you want to track it in devtools, then it is fine to add it to the Vuex store; it’s not a violation of the pattern.

Props → State

If you’ve modularised your application into components, as I have with Vue.js Cinema, a good place to start looking for application state is your component props, and more specifically, props across multiple components that share the same data.

For example, I bound the data property day from the root instance of this application to the day-select and movie-list components. The day select is how a user selects what day of the week they want to see session times for, and the movie list is filtered by the selected value.

The day-select component

To manage day in Vuex we can simply delete it from the root instance and move it to the state object in the Vuex store:

export default new Vuex.store({
  state: {
    day: moment() // the initial value
  },
  ...
});

We can then delete the binding from the template i.e. v-bind:day="day", and in the component, replace the prop with a computed property that calls the store. In the case of day-select:

export default {
  props: [ 'day' ],
  ...
}

Goes to:

export default {
  computed: {
    day() {
      return this.$store.state.day
    }
  }
}

Events → Mutations

While props are about sharing data across components, events intend to change it. The event listeners in your code should be considered for conversion to Vuex mutations. Event emitters will then be refactored to commit a mutation, rather than emit an event.

To return to the example of the day-select component, whenever a user changes the day by clicking the appropriate piece of UI, this component method is called:

selectDay(day) {
  this.$emit('setDay', day);
}

The root instance has a listener to respond to the custom setDay event:

created() {
  this.$on('setDay', day => { this.day = day; });
}

In the case of the listener, it can be transferred from the root instance and put into the mutations object of the store with only slight modification:

src/store/index.js

export default new Vuex.Store({
  state: {
    day: moment()
  },
  mutations: {
    setDay(state, day) {
      state.day = day;
    }
  }

A mutation commit will now replace the event emit in the day-select component:

selectDay(day) {
  this.$store.commit('setDay', day);
}

We can now remove the event listener binding in the template as well i.e. v-on:click="setDay".

AJAX → Actions

Actions in Vuex are the mechanism for handling asynchronous mutations. If you’re using AJAX to update your application state, you’ll probably want to wrap it in an action.

In Vue.js Cinema, I use AJAX (via the Vue Resource HTTP client) to populate the movie data from an API endpoint on my server:

src/main.js

created() {
  this.$http.get('/api').then(response => {
    this.movies = response.data;
  }); 
}

This only happens once in the application lifecycle, when it is created, so it does seem somewhat trivial to log this with Vuex, but there’s no harm, and still a lot to gain in terms of debugging.

Actions are dispatched from the application and must commit a mutation once the asynchronous event completes. Here’s the code to add to the store:

src/store/index.js

export default new Vuex.Store({
  state: {
    ...
    movies: []
  },
  mutations: {
    ...
    setMovies(state, movies) {
      state.movies = movies;
    }
  },
  actions: {
    getMovies({ commit }) {
      Vue.http.get('/api').then(response => {
        commit('setMovies', response.data);
      });
    }
  }
});

The created hook will now just dispatch an action:

src/main.js

created() {
  this.$store.dispatch('getMovies');
}

Results and benefits

Once Vue.js Cinema has been migrated to Vuex, what are the tangible benefits? There’s probably not going to be any direct benefits to the user in terms of better performance etc, but it will make the life of the developers easier.

Debugging

Now that the application data is being managed by Vuex, we can easily view any changes to it in Vue Devtools:

Vuex and Vue Devtools not only log a change, and the source of the change, but allow you to “rewind” the change so you can see exactly how it affects the application.

As creator Evan You said: “The benefit of Vuex is that changes going through the store are trackable, replayable and restorable.”

Decoupling of components and state

In Vue.js Cinema, we track the checked filters in two arrays, one for time and one for genre. Adding and removing filters requires a function that was previously a method on the root instance. Using Vuex, we can abstract that logic into a mutation:

src/store/index.js

mutations: {
    ...
    checkFilter(state, { category, title, checked }) {
      if (checked) {
        state[category].push(title);
      } else {
        let index = state[category].indexOf(title);
        if (index > -1) {
          state[category].splice(index, 1);
        }
      }
    }
  },

The advantage of this kind of decoupling is that it makes the code more logical and maintainable.

Condensing templates

Without a store pattern like Vuex, we share data between components through the mechanisms of props and events, which need to be declared in a component’s template. In a large application this can make templates quite verbose.

Managing shared data in Vuex allows this:

<movie-list
  :genre="genre" 
  :time="time" 
  :movies="movies" 
  :day="day"
></movie-list>

To go to:

<movie-list></movie-list>
Anthony Gore's Picture

About Anthony Gore

Anthony is a web developer and online course instructor with a passion for Javascript. He currently resides in Chiang Mai, Thailand.

Comments