Managing Vue state in Vue.js applications with Vuex

Posted by: Daniel Jimenez Garcia , on 8/6/2018, in Category Vue.js
Views: 4228
Abstract: Vuex provides an alternate state management approach to your Vue.js applications that makes it very interesting on large apps. This tutorial takes a deeper look at Vuex concepts

As you write applications with Vue and take advantage of its support for components, you will need to decide how to manage the state of your application.

You will need to answer questions such as:

  • Which component holds each piece of data?
  • How is that data shared between components?
  • How are changes to the data communicated across those components?

Editorial Note: Are you new to Vue.js? Check out www.dotnetcurry.com/javascript/1349/introducing-vue-js-tutorial.

The traditional answer to these questions would be to keep your state as part of the instance data of a component. This will be passed down as input properties to other components, establishing a hierarchy of parent and child components. In turn, child components can communicate back changes via events.

This tutorial is from the DotNetCurry(DNC) Magazine with in-depth tutorials and best practices in JavaScript and .NET. This magazine is aimed at Developers, Architects and Technical Managers and covers Angular, React, Vue.js, C#, Design Patterns, .NET Core, MVC, Azure, DevOps, ALM, and more. Subscribe to this magazine for FREE and receive all previous, current and upcoming editions, right in your Inbox. No Spam Policy.

While intuitive, this approach will show its problems in large and complex applications.

Events will make it harder to reason about your state and how it changes. Passing down the same piece of data across a big hierarchy of components becomes painful.

Some pieces of data won’t even adapt well to this parent-child approach, as they become part of a context shared by many components like a shopping cart in an e-commerce application.

This is when centralized state management techniques become useful.

Vuex is the officially endorsed library for Vue in these matters. During this article we will look at how state is managed with Vuex, which is an excellent tool at your disposal for scenarios where the default state management isn’t enough.

The companion source code is available in github.

Using Vuex to manage Vue State

Adding Vuex to your project

Vuex is available on both npm and yarn from where you can install it as any other dependency:

npm install vuex –save
yarn add vuex

Now in your main file for the client side, make sure Vue is aware that you will be using Vuex:

import Vue from 'vue';
import Vuex from 'vuex';
…
Vue.use(Vuex);

That’s everything you need to do in order to start using Vuex in an existing Vue project. You should now be able to create a simple Vuex store and inject it into your application:

const store = new Vuex.Store({
  state: {
    message: ‘Hello World’
  },
});

new Vue({
  router,
  store,
  render: h => h(App),
}).$mount('#app');

By providing the store to the root Vue instance, it will be injected as $store to any component part of your app. This way every one of your components can gain access to the centralized state provided by your store. For example, you could build a hello-world component like:

<template>
  <p>{{ message }}</p>
</template>
<script>
export default {
  computed: {
    message() {
      return this.$store.state.message;
    },
  },
};
</script>

Notice how this component declares a computed where it exposes the message property of the centralized state. Because any state property follows the Vue reactivity rules, if the state was changed in the store, the computed would be re-evaluated as well and the new message would be rendered.

We are now ready to take a deeper look at Vuex concepts that will help you access and mutate the data in your store and structure bigger applications. As a final note, if you want you can easily create a new project with Vue and Vuex using the vue-cli and selecting Vuex while initializing the project.

Vuex relies on ES6, which shouldn’t be a problem since most project templates using Vue will include a bundler like webpack and a transpiler like Babel as the Vue ecosystem relies on ES6 as well. If you are manually adding libraries to an existing/legacy project you will want to make sure you add bundling/transpiling support. If you want to follow along, using the Vue CLI is the easiest option.

Vuex concepts

Centralized State Management

You can think of Vuex as a global singleton where you keep the state of your application.

This global singleton is called store by Vuex and will be injected in your components, so they can access the bits of data they need. This being Vue, it shouldn’t come as a surprise the data kept by a store is reactive. Changes to the state will re-render dependent templates and re-evaluate dependent computeds.

Imagine a simple parent-child component hierarchy where some data is passed to the child component, where it might be modified by the user. The typical Vue answer is that the data is passed from the parent to the child as props, while any changes are communicated back via events:

parent-child-components

Figure 1: classic parent-child components

If we implement the same component hierarchy using Vuex, the need for events to communicate back changes disappear, as they both use the same central store where the data will be modified.

Now it is even optional to share the data using props, as both can read it from the state:

vuex-parent-child

Figure 2: parent-child components with a Vuex store

One shared singleton where everyone reads from and modifies the data can become messy. This is why Vuex provides a concise number of rules and concepts that allow you to manage this complexity in a simple way:

  • Data is kept as part of the store’s state.
  • The state can only be modified through mutations. This goes as far as Vuex providing an optional strict mode that will raise errors if state changes outside a mutation.

Those are the two core rules to have a centralized store, but Vuex provides additional concepts that will help you further deal with complexity while keeping a centralized store:

  • Derived data can be provided through getters, the Vuex equivalent to a computed.
  • Mutations must be synchronous.
  • Asynchronous dependencies are handled through actions.
  • The usage of modules will help you develop big stores and provide namespacing.

We will take a closer look to these concepts through the following sections.

State

The state is the heart of any Vuex store, it is the data kept by the store. Earlier, we have seen the definition of a Vuex store whose state contained one property called “message”:

const store = new Vuex.Store({
  state: {
    message: ‘Hello World’
  },
});

Any of your Vue components can now get access to the value of the message property as in this.$store.state.message. For example, you can declare a computed property:

export default {
  computed: {
    message() {
      return this.$store.state.message;
    },
  },
};

Since this can get very verbose and repetitive, Vuex provides you with a simpler mapState helper that achieves the same result:

import { mapState } from ‘vuex’; 
export default {
  computed: {
    ...mapState([
      ‘message’
    ]),
  },
};

Not recognizing the syntax? Since mapState returns an object with a property per each mapped state’s property, we can use the ES6 object spread operator (the 3 dots in front of mapState) to automatically merge those properties with any other computed property declared by our component.

This helper is useful when mapping multiple properties from the store as it lets you map them as computed properties in a very concise way by adding more property names to the array. It can also receive an object that lets you even more interesting mappings like renaming properties or deriving a new value:

import { mapState } from ‘vuex’; 
export default {
  data(){
    prepend: ‘The store message is:’
  },
  computed: {
    ...mapState({
      message: ‘message’ // same as before
      originalMessage: state => state.message,
      lowerCaseMessage: state => state.message.toLowerCase(),
      combinedMessage: state => this.prepend + ‘ ’ + state.message
    }),
  },
};

One of the most important characteristics of the state is its reactivity.

Any changes to a property of the state will be detected, computed properties will be evaluated and templates rendered with the new values. This also means the state follows exactly the same reactivity rules as for component’s data, with all of its caveats!

Getters

We know that data is kept in the store state and that it can be read inside components by either accessing $store.state or using the mapState helper. A common requirement will be then to generate derived data from the raw data kept in the store.

While we could generate the derived data using the mapState helper, or declare a computed property where we calculate it, that would not work if we need the same derived state used in multiple components. Every component would duplicate the mapState declaration or the computed! This is why Vuex provides getters, as they allow you to generate derived data which can then be used in any component.

Imagine a store that holds a user profile with its first and last name. With a getter, you could also provide its full name or a message to greet him:

const store = new Vuex.Store({
  state: {
    profile: {
      firstName: ‘Terry’,
      lastName: ‘Pratchett’
    }
  },
  getters: {
    fullName: state => {
      return `${state.profile.firstName} ${state.profile.lastName}`;
    },
    welcomeMessage: (state, getters) => {
      // getters can not just use the state
      // they can use other getters
      return `Hello ${getters.fullName}`;
    }
  }
});

This way you can map in any component not just the profile kept in the store, but also the derived fullName or welcomeMessage. With the state, you could directly access $store.getters.fullName or you could use the handy mapGetters helper provided:

import { mapGetters } from ‘vuex’; 
export default {
  computed: {
    // you can directly access any getter in your component
    // by accessing this.$store.getters
    manualAccess(){
      return this.$store.getters.fullName;
    }
    // or you can use mapGetters to create a computed with
    // the same name than the getter
    ...mapGetters([
      ‘fullName’,
      ‘welcomeMessage’
    ])
    // you can also use mapGetters to create a computed
    // with a different name
    ...mapGetters({
      renamedMessage: ‘welcomeMessage’
    }),
  },
};

Data provided through a getter is also reactive. When the state properties they depend upon change, the getters will be re-evaluated.

The getters we have seen so far depend exclusively on the store state. However, it is also possible for a getter to receive external parameters. For example, imagine the store also holds the date of birth. We might need to display the age that user had when he performed a certain action, which is something we can accomplish with a getter that receives a date parameter:

const store = new Vuex.Store({
  state: {
    profile: {
      firstName: ‘Terry’,
      lastName: ‘Pratchett’,
      dateOfBirth: new Date(‘1948-04-28’)
    }
  },
  getters: {
    getAgeInDate: state => date => {
      let age = date.getFullYear() – state.dateOfBirth.getFullYear();
      const m = date.getMonth() - state.dateOfBirth.getMonth();
      if (m < 0 
        || (m === 0 && date.getDate() < state.dateOfBirth.getDate())) {
        age--;
      }
      return age;
    },
  }
});

This getter can be accessed in a component in a very similar way to the previous getters we have seen. The main difference is that rather than being used as a computed property, it should be used as a method.

This means we can either call it as this.$store.getters.getAgeInDate(date) or we can map it as a method using the mapGetters helper:

import { mapGetters } from ‘vuex’; 
export default {
  props: [
    ‘book’
  ],
  computed: {
    // you can directly access any getter in your component
    // by accessing this.$store.getters
    manualAccess(){
      return this.$store.getters.getAgeInDate(this.book.releaseDate);
    },
    // you can also map as a method in the methods section
    // and use it as any other method of the component
    mappingAccess(){
      return this.getAgeInDate(this.book.releaseDate);
    }
  },
  methods: {
    // as usual, pass an array when keeping the same getter name
    // or pass an object where you rename the getters
    ...mapGetters([
      ‘getAgeInDate’,
    ])
  }
};

Now you might wonder how is the state modified or even initially populated? Let’s shed light by looking at the mutations and the actions.

Mutations

In the examples we have seen so far, the store state already contained the data. For example, the profile store contained a hardcoded name and date of birth as state. Having hardcoded data isn’t useful which means we need to set and update the store state.

Vuex provides mutations as the only way of updating the store state. If we go back to our profile example, we can initially declare the profile as an empty object and provide a mutation to set it:

const store = new Vuex.Store({
  state: {
    profile: {}
  },
  mutations: {
    setProfile(state, profile){
      state.profile = profile;
    }
  }
});

Rather than directly invoking or executing a mutation, mutations are committed in Vuex. All that means is that you don’t have direct access to a mutation function, instead the store provides a single commit method that lets you invoke any mutation defined in the store as in commit(‘mutationName’, payload).

As you might be familiar by now, inside a component, you can directly commit the mutation through this.$store.commit(‘setProfile’, profile) or you can use the mapMutations helper which will map them for your as methods of your component:

import { mapMutations } from ‘vuex’; 
export default {
  data(){
    return {
      profile: {
        firstName: ‘Terry’,
        lastName: ‘Pratchett’
      }
    };
  },
  created(){
    // initialize the store state when the component is created
    // we can manually commit the mutation
    this.$store.commit(‘setProfile’, this.profile);
    // or we can map it below and use it as if it were a component
    // method named setProfile
    this.setProfile(this.profile);
  }
  methods: {
    // as usual, pass an array if you want to keep the mutation name
    // or pass an object where you can rename it
    ...mapMutations([
      ‘setProfile’,
    ])
  }
};

We have already mentioned that the store state is reactive and follows the same reactivity rules and caveats of Vue. If you go back to our mutation example, you will notice that the state was initialized with an empty profile object. This way the state’s profile is made reactive by Vue and when we replace it with another object, the dependent observers like computed properties can be notified of the change.

- This is one of Vue reactivity caveats that might take you by surprise. By declaring the state as profile: {}, Vue will make the profile property reactive but will know nothing of whatever inner properties the profile might have, once it is set (like the firstName and lastName).

- In fact if our setProfile mutation was written as Object.assign(state.profile, profile) or even state.firstName = profile.firstName; etc then Vue won’t be able to detect any changes! That’s why the mutation is replacing the state.profile object with a different one, so Vue realizes the object has changed.

- For individual properties to be reactive (so we can update them individually rather than replacing the entire profile object), declare them in the initial state as in:

const store = new Vuex.Store({
  state: {
    profile: {
      firstName: null,
      lastName: null,
    }
  },
  mutations: {
    setProfile(state, profile){
      state.profile = profile; //ok
      Object.assign(state.profile, profile); // now ok too!
      state.profile.firstName = profile.firstName; // now ok too!
    }
  }
});

There is one additional characteristic about the mutations to be discussed before we move on. The mutations need to be synchronous!

This is a fundamental restriction on mutations, which was added by design. This way mutations are straightforward and easy to reason about, and development tools can provide a log of mutations including the state before and after the mutation.

Our application can deal with dependencies like HTTP APIs or browser events using Asynchronous code. That’s the reason Vuex also provides actions.

Actions

We have seen mutations are the only way of modifying the store state, however they must be synchronous. This is a huge restriction since dealing with asynchronous dependencies is a common requirement in any web application, including fetching data from a server or interacting with browser events and APIs.

Vuex solution to this problem are actions, whose implementation can be asynchronous. The caveat is that, since mutations are the only ones who can modify the state, actions need to dispatch mutations themselves to modify the state (they cannot change it directly).

Their definition is very similar to that of the mutations, with the difference that instead of receiving the state as argument, they receive a context object that gives them access to the commit function, the getters and the current state (but remember they can’t change it themselves).

As you can see in our extended profile example, defining an action that loads a profile from the server and commits it to the store is straightforward:

import axios from ‘axios’;
const store = new Vuex.Store({
  state: {
    profile: {}
  },
  mutations: {
    setProfile(state, profile){
      state.profile = profile;
    }
  },
  actions: {
    loadProfile({commit}, profileId){
      return axios.get(`/my/get/profile/route/${profileId}`).then(response => {
        commit(‘setProfile’, response.data);
      });
    }
  }
});

Not recognizing the syntax? The first argument is the mentioned context object which provides access to the Vuex store, i.e. it is an object like {commit, dispatch, getters, state}. Rather than naming its context and use it as context.commit(), we can use ES6 object destructuring and directly extract the properties we need from that object, in this case the commit() function.

As you can see the action definition is straightforward and we are free to write any asynchronous code. This example uses the axios HTTP GET method to load the profile JSON from the server. You are free to use your preferred library. All Vuex cares is that changes to the state should be handled by committing a mutation.

Similar to mutations, the store actions are not directly invoked. Instead actions are dispatched, for which the store provides a central dispatch method that can be used as in

store.dispatch(‘actionName’, payload).

And I am sure you have guessed this by now, to dispatch an action from a component, you can directly dispatch it through this.$store.dispatch(‘loadProfile, profileId) or you can use the mapActions helper which will map them for you as methods of your component. This way we can now update the definition of our component, where we will ask the store to load the profile from the server. The necessary profileId could be read from the current route parameters or received as an input property:

import { mapActions } from ‘vuex’; 
export default {
  props: [
    ‘profileId’
  ],
  created(){
    // ask the store to load the required profile on creation
    // we can manually dispatch the action
    this.$store.dispatch(‘loadProfile’, this.profileId);
    // or we can map it below and use it as if it were a component
    // method named loadProfile
    this.loadProfile(this.profileId);
  }
  methods: {
    // as usual, pass an array if you want to keep the action name
    // or pass an object where you can rename it
    ...mapActions([
      ‘loadProfile’,
    ])
  }
};

Before we move on, lets expand on an important point when writing actions dealing with Promises for asynchronous code, which might not be immediately obvious.

Whenever you use promises in an action (as when using axios HTTP request methods which all return promises), you want to return the promise from the action:

loadProfile({commit}, profileId){
  return axios.get(`/my/get/profile/route/${profileId}`).then(response => {
    commit(‘setProfile’, response.data);
  });
}

Notice the actions has a return statement, so when the action is dispatched the promise is returned to the caller. The simple inclusion of the return statement allows you to:

- Chain additional behavior from your component. For example, show a spinner or progress indicator while the request is in progress:

data(){
  return { loading: true };
},
created(){
  this.loadProfile(this.profileId).then(() => {
    this.loading=false;
  });
}

- Compose multiple actions. For example, we could create a second action that first loads the profile and then also loads some user preferences. Notice how the new action loadUserContext will itself dispatch the loadProfile and then perform an additional request to fetch the user preferences:

loadProfile({commit}, profileId){
  return axios.get(`/my/get/profile/route/${profileId}`).then(response => {
    commit(‘setProfile’, response.data);
  });
},
loadUserContext({commit, dispatch}, profileId){
  return dispatch(‘loadProfile’, profileId)
    .then(() => return axios.get(`/my/get/preferences/route/${profileId}`))
    .then(response => {
      commit(‘setPreferences’, response.data);
    });
}

And with these last points we have finished the overview of the main Vuex concepts. We will now look at a practical example before finishing up with tools and strategies for managing big Vuex stores.

Practical example: TODO list with Vuex

Creating a Vue project with Vuex

The easiest way to generate a Vue project with Vuex as a starting point, is to use the Vue CLI. Once the CLI has been installed, create a new project as in vue create vuex-todo. When asked by the create command to pick a preset, make sure to select manually select features and then select Vuex and vue-router.

Next, install axios as we will use it to send HTTP requests, so run the following command npm install --save axios from the command line.

Once you are ready generating the project and installing the libraries, open the source code in your favorite editor and you should see a minimum Vue application including a router and a Vuex store:

vue-cli-generated-project

Figure 3: the project generated by the Vue CLI

Notice how main.js imports the store and injects it on the root Vue instance, so it is available on every component through the this.$store variable (and the Vuex mapping helpers):

import store from './store';
new Vue({
  router,
  store,
  render: h => h(App),
}).$mount('#app');

The store itself will be ready for you to implement it. Right now, it imports and configures Vuex and provides empty state, actions and mutations declarations. Notice how it tells Vue you want to use Vuex and how the store itself is exported so main.js can inject it on the root Vue instance:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);


export default new Vuex.Store({
  state: {

  },
  mutations: {

  },
  actions: {

  },
});

With our project ready, let’s make changes. The first steps we will take are:

  • Adding a new page /src/views/Todos.vue
  • Adding a new route /todos inside src/router.js that is associated with the Todos component
  • Finally updating the navigation inside App.vue with a new router-link for the todos route we just created.

It should be easy to follow the example of the existing routes/pages but if you run into trouble, feel free to check the code on github.

At this stage, our Todos.vue component will be as simple as they come, displaying a static header:

<template>
  <div class="todos">
    <h1>This is my list of things TODO</h1>
  </div>
</template>

<script>
export default {
  name: 'todos',
};
</script>

Start the project by running npm run serve from the command line, you should see a message telling you the app is running at http://localhost:8080 so open that url in your favourite browser:

running-vuex-project

Figure 4, running the project with the new empty page and route

Now we have both an empty page and empty Vuex store where we can start implementing our TODO list.

Displaying the list of TODO items

We will start by fetching the initial list of TODO items from an online test API that comes very handy when building a front-end without a server-side API available. It has a /todo API that can be used to retrieve a list of 200 TODO items, modify them and add new ones (although changes are not persisted, it is just meant to prototype front-ends).

We will start from the Vuex store, declaring an empty array of TODO items as the initial state, providing a mutation to update such array, and an action to fetch them from our newly found API:

import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    // each todo has { id, title, completed } properties
    todos: [],
  },
  mutations: {
    setTodos(state, todos){
      state.todos = todos;
    },
  },
  actions: {
    loadTodos({commit}){
      return axios.get('https://jsonplaceholder.typicode.com/todos')
        .then(response => {
          // get just the first 10 and commit them to the store
          const todos = response.data.slice(0, 10);
          commit('setTodos', todos);
        });
    },
  },
});

Everything so far should feel familiar as we have already discussed the role of the state, mutations and actions. The only thing worth discussing is the fact we are just taking the first 10 TODO items from the response as we don’t need the 200 that test API returns.

Let’s now go to our Todos.vue and integrate the store. The first thing we will do is map the todos array from the store state and map the loadTodos action. Now we can dispatch the action as soon as the component is created:

import { mapActions, mapState } from 'vuex';

export default {
  name: 'todos',  
  created(){
    this.loadTodos();
  },
  computed: {
    ... mapState([
      'todos'
    ])
  },
  methods: {
    ...mapActions([
      'loadTodos'
    ])
  },
};

Next, we will create a separated component to render a single TODO item. Create a new /todos folder inside src/components and add a new file TodoItem.vue inside. All this component needs to do for now is to render a list item with the TODO title, display the completed ones with a strike through line:

<template>
  <li :class="itemClass">
    {{ todo.title }}
  </li>
</template>

<script>
export default {
  name: 'todo-item',
  props: [
    'todo',
  ],
  computed: {
    itemClass(){
      return {
        'todo-item': true,
        completed: this.todo.completed,
      };
    }
  },
};
</script>

<style scoped>
.todo-item{
  margin: 1rem 0;
  font-size: 16px;
}
.todo-item.completed{
  text-decoration: line-through;
}
</style>

Nothing remarkable here, a plain and boring Vue component to render a single TODO item. Now we can go back to the main Todos.vue and use it to render each of the items we load from the API. Update the template section of Todos.vue so it looks like:

<div class="todos">
  <h1>This is my list of things TODO</h1>  
  <ul class="todo-list">
    <todo-item v-for="todo in todos" :key="todo.id" :todo="todo" />
  </ul>
</div>

Before we can use the todo-item component in our template like we did above, we need to update the script section of Todos.vue. Import the component and provide it in the components property:

import TodoItem from '@/components/todos/TodoItem.vue';
...
export default {
  name: 'todos',
  components: {
    TodoItem,
  },
  ...
}

That’s all we need.

Now when you go to the /todos page, the Todos.vue component is instantiated, which will call the loadTodos action. Once the action completes, the list of TODO items will be committed to the store, at which point the Todos.vue template will re-render, with a <todo-item /> component rendered for each item.

Give a try at this point!

Before moving on, let’s take further advantage of our store. Let’s update the store with a new incompleteCount getter that returns the count of items to be completed:

export default new Vuex.Store({
  ...
  getters: {
    incompleteCount: state => {
      return state.todos.filter(todo => !todo.completed).length;
    },
  },
  ...
});

Then map it on the Todos.vue component:

export default {
  name: 'todos',
  ...
  computed: {
    ...mapGetters([
      'incompleteCount'
    ])
  },
  ...
};

And finally update the template to include a message with the number of items still to be completed, including the following code right after the current h1 header:

<h3>I still have {{incompleteCount}} things to be completed!</h3>

rendering-list

Figure 5: rendering the list of TODO items retrieved from the server

We are done with loading and rendering the list of items, lets move onto being able to complete an item.

Completing a TODO item

To complete a TODO item, we need a new action that will send a request to the server for modifying the item. Once the request succeeds, we can commit a new mutation that will update the completed flag of the item in the store:

export default new Vuex.Store({
  state: {
    // each todo has { id, title, completed } properties
    todos: [],
  },
  ...
  mutations: {
    completeTodo(state, todoId){
      const todo = state.todos.find(todo => todo.id === todoId);
      todo.completed = true;
    },
    ...
  },
  actions: {
    completeTodo({commit}, todoId){
      return axios.put(`https://jsonplaceholder.typicode.com/todos/${todoId}`)
        .then(() => {        
          commit('completeTodo', todoId);
        });
    },
    ...
  },
});

Now all that’s left is updating the TodoItem.vue component. We map the new completeTodo action, and we render a link besides the element which when clicked will dispatch the action:

<template>
  <li :class="itemClass">
    {{ todo.title }}
    <a v-if="!todo.completed" href="#" @click.prevent="onComplete">
      complete
    </a>
  </li>
</template>

<script>
import { mapActions } from 'vuex';

export default {
  name: 'todo-item',
  ...
  methods: {
    ...mapActions([
      'completeTodo',
    ]),
    onComplete(){
      this.completeTodo(this.todo.id);
    },
  },
};
</script>
completing-vue-items

Figure 6: setting TODO items as completed

And that’s it, now our TODO items can be updated as completed. Notice how the count of incomplete items automatically updates whenever an item is updated in the store!

 

 

Adding a new TODO item

We will be following the same approach as in the previous section, starting by the changes to the store. We need a new action that will post the new TODO item to the server and commit a mutation once the response comes back:

export default new Vuex.Store({
  state: {
    // each todo has { id, title, completed } properties
    todos: [],
  },
  ...
  mutations: {
    ...
    addTodo(state, todo){
      state.todos = [todo, ...state.todos];
    },
  },
  actions: {
    ...
    addTodo({commit, state}, todo){
      return axios.post('https://jsonplaceholder.typicode.com/todos')
        .then(response => {        
          commit('addTodo', {
            title: todo.title, 
            completed: false, 
            // ignore the real id from the response. 
            // it always returns id=201 but the item isn’t really created
            // on the server so we get 404 when trying to complete it
            id: state.todos.length,
          });
      });
    },
  },
});

There are two points worth discussing from the code above.

  • First, look carefully at the way the mutation is written. You will see the state.todos is assigned to a new array with the new todo as the first item. By reassigning the todos property of the state, we follow Vue reactivity rules and this way it will render the new <li>. And because we used a :key="todo.id" within the v-for statement of the template, Vue will know which items were present in the original array.
  • Next, we have hit one limitation of the test API we are using. The POST endpoint is functional, but it always returns an object with id=201, which means all new TODO items will have the same id. Even worse, the items aren’t created in the server, so we would get a 404 if we try to update them later as completed. To avoid this issue, since we only loaded the first ten items with ids from 1 to 10, we are assigning an id ourselves based on the list length.

Now we will create a new component called NewTodo.vue, where we will render an input to capture the title and will dispatch the addTodo action whenever the user clicks a link:

<template>
  <div class="new-todo">
    <input type="text" v-model="title" />
    <a href="#" @click.prevent="onAdd">add one more!</a>
  </div>
</template>

<script>
import { mapActions } from 'vuex';
export default {
  name: 'new-todo',
  data(){
    return {
      title: '',
    };
  },
  methods: {
    ...mapActions([
      'addTodo',
    ]),
    onAdd(){
      this.addTodo({title: this.title}).then(() => {
        this.title = '';
      });
    },
  },
};
</script>

<style scoped>
.new-todo a{
  margin-left: 10px;
}
</style>

All that is left is to register this new component with the parent Todos.vue component (same as we did with TodoItem.vue) and update this template to simply include <new-todo /> right above the list.

adding-new-items

Figure 7: adding new TODO items to the list

This completes our practical example of a TODO list using Vuex! If you had any trouble following along or simply want to look at the end result, feel free to check the code in GitHub.

Structuring large stores

So far, we have seen everything you need to use Vuex, and we have put what we learned into action through the classic TODO list example.

As your application grows and you add properties to the state, mutations and actions; it might get to a point where it is hard to manage such a large store or even ensure you don’t have naming clashes!

This section briefly discusses further tools and techniques that will help you scale your Vuex store as your application grows.

Using constants for getters/mutations/actions names

If you are like me, you might have been looking with certain dislike at all the mapper helpers we have used, which relied on magic strings that should match the name of the real state, getters, mutations and actions in the store.

However, we can declare all the names in separate constants file:

export const mutations = {  
  SET_TODOS: 'setTodos'
  ...
}
export const actions = {  
  LOAD_TODOS: 'loadTodos'
  ...
}
export const getters = {  
  ...
}

Then use ES6 features when declaring the store properties:

import {mutations, actions} from './constants';
export default new Vuex.Store({
  ...
  mutations: {    
    [mutations.SET_TODOS](state, todos){
      state.todos = todos;
    },
    ...
  },
  actions: {
    [actions.LOAD_TODOS]({commit}){
      return axios.get('https://jsonplaceholder.typicode.com/todos')
        .then(response => {
          // get just the first 10 and commit them to the store
          const todos = response.data.slice(0, 10);
          commit(mutations.SET_TODOS, todos);
        });
    },
  },
});

Notice how the constants are imported and how ES6 allows us to define the actions/mutations/getters using the constant names as in [actions.LOAD_TODOS]({commit}){ rather than the standard loadTodos({commit}){ syntax. Also notice how when the action commits the mutation, it uses the constant.

Note: I wouldn’t recommend using constants for the state properties. Doing so would make the code of the actions, getters and mutations hard to follow, since they won’t be able to access state properties.

Additionally, we can use the same constants when using the mapping helpers in your components:

...
import { actions } from '@/constants';

export default {
  name: 'todos',
  ...
  methods: {
    ...mapActions([
      actions.LOAD_TODOS,
    ]),
  },
};

With this simple trick, you can avoid hardcoded strings across your components.

Vuex Modules

Another thing that will strike you is that defining the entire store and all its properties, getters, mutations and actions within a single file is not practical and won’t scale well.

You could easily split them into multiple files which will be imported and combined into one store within a root store.js file. However, this would only solve the single long file problem:

  • You might still find it hard to avoid clashes with state/action/mutation names.
  • From any given component, it won’t be easy to find the code for an action/mutation within your multiple files unless you follow naming convention.

To help you with these issues, Vuex provides the notion of modules. A module is basically its own store, with state, getters, mutations and actions, which is merged into a parent store.

// Inside /modules/todos.js
export default {
  state: {
    todos: []
  },
  getters: { ... },
  mutations: { ... },
  actions: { ... }
}

// Inside the root store.js
import todosModule from './modules/todos';
import anotherModule from './modules/another';
export default new Vuex.Store({
  modules: {
    todos: todosModule,
    another: anotherModule
  }
});

In its simplest usage, they are just a formal way of splitting your store code in multiple files.

However, they can also opt-in for namespacing which helps to solve the naming issues. If a module declares namespaced: true as part of its declaration, the names for state properties, getters, actions and mutations will be all namespaced to the module name.

When a namespaced module is imported by the root store as in todos: todosModule, all the resulting state, getter, action and mutation names will be prefixed with the given module name, in this case todos/. That means instead of the loadTodos action, you will have the todos/loadTodos action and so on.

Since namespacing is a common usage of Vuex, all the mapping helpers support a first argument with the name of the module you want to map from:

// Inside /modules/todos.js
export default {
  namespaced: true,
  state: {
    todos: []
  },
  getters: { ... },
  mutations: { ... },
  actions: { ... }
}

// Inside a component
computed: {
  ...mapState('todos', [
    'todos',
  ]),
},
methods: {
  ...mapActions('todos', [
    'loadTodos',
  ]),
},

Notice how the mapping helpers have a first parameter with the name of the module. Plain modules and namespaced modules allow you to scale your Vuex store as your application gets developed and more functional areas are written.

Chrome dev tools

Not just when using Vuex, but when using Vue in general, it is handy to have the Vue.js dev tools chrome extension installed. This will automatically detect when Vue is being used on a page and will let you inspect the component tree, including each component data, and the flow of events.

chrome-vue-dev-tools

Figure 8: the Chrome dev tools

When using Vuex, the dev tools will automatically display any state/getters mapped from the store on each component. You even get a dedicated tab to the store that will display the current state and the flow of mutations that were committed.

inspecting-vuex-state.

Figure 9: inspecting the vuex state

It might not be immediately obvious that within this tab, you can select any state from the Base State to any of the mutations that were committed and inspect the state at that time and the mutation payload!

As you can see, this tool can be very helpful not just during debugging sessions, but also for newcomers to better understand what’s going on in their applications.

Conclusion

Vuex provides an alternate state management approach to your Vue applications that makes it very interesting on large apps once the number of components grows and it’s hard to keep track of the flow of data and events between components.

In my projects, using Vuex has allowed me to keep the components focused on rendering data and reacting to user events. I have also found a natural place for shared data which doesn’t have a clear owner in a pure component hierarchy. In complex pages with deep component hierarchies, I have avoided the feared mess of properties passed deep down the component hierarchy and events delegated up.

At which point does Vuex help rather than add unnecessary complexity is something that varies depending on the requirements and team expertise.

The good news is that following Vue’s philosophy, you don’t have to go all in with Vuex. Instead, you can mix and match Vuex with the standard props and events approach in different areas of your application. This allows you to introduce Vuex only in areas where/when you clearly see the benefit, like a shopping cart in an e-commerce application.

If you are using Vue, I would encourage you to spend some time understanding Vuex. At first, it might seem like yet another library you don’t need, but as you encounter the limitations of the components props and events, you will be grateful you have an alternative at your disposal!

This article was technically reviewed by Damir Arh.

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Daniel Jimenez Garcia is a passionate software developer with 10+ years of experience. He started as a Microsoft developer and learned to love C# in general and ASP.NET MVC in particular. In the latter half of his career he worked on a broader set of technologies and platforms while these days he is particularly interested in .Net Core and Node.js. He is always looking for better practices and can be seen answering questions on Stack Overflow.


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!

Categories

JOIN OUR COMMUNITY

POPULAR ARTICLES

FREE .NET MAGAZINES

Free DNC .NET Magazine

Tags

JQUERY COOKBOOK

jQuery CookBook