Getting started with Vuex state management for VueJS

vuejs.png

In this tutorial, we will learn about the new VueJS library called Vuex and it's core concepts along with an use case where Vuex is useful and how you can use it.

Introduction

VueJS is a library for building interactive web interfaces. It provides data-reactive components with a simple and flexible API. Many developers use Vue because it is lightweight and easy to wrap your head around.

So where’s the problem? Well, developers often need to alter a parent data/property from a child component. In Vue, we might need to use an accessor function such as this.$parent.data. We could also keep emitting events from the child component to the parent component.

While this might be a simple way to alter a parent data, it becomes stressful as the code base increases. What happens if the data gets used across several components? If we have 30 components it would mean us emitting 30 different events. Not only does this mean more chance for error, we would need to spend more time debugging and we end up repeating similar code 30 times.

Now we have identified our problem, how should we solve it?

Answer: Vuex. Vuex makes it possible to use a centralized state management in your application. Vuex is a library which helps you to enforce a Flux-like application architecture in your Vue application.

What is Vuex?
According to the official documentation for Vuex, it is a state management pattern and library for VueJS applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only get mutated in a predictable fashion.

In Vuex, the object created is called a store. Before going further, let us acquaint ourselves with some core concepts:

  • State: This refers to a JavaScript object containing the data we want to use in our store. It is like the data attribute of a Vue component.

Vuex uses a single state tree. This single object contains all your application-level state and serves as the “single source of truth”. This also means you will have only one store for each application. A single state tree makes it straightforward to locate a specific piece of state and allows us to take snapshots of the current app state for debugging purposes.

  • Actions: This refers to functions that commit mutations. Unlike Mutations, Actions can contain asynchronous operations. As of Vuex 2.x, Actions are dispatched. Actions are like the “methods” block of our usual Vue component.
  • Mutations: This refers to functions that can change our state. The only way to change state in a Vuex store is by committing a mutation. Vuex mutations are like events: each mutation has a string type and a handler. The handler function is where we perform actual state modifications, and it will receive the state as the first argument.
  • Getters: This refers to helper functions that get data from our states. You can think of them as computed properties for stores. Like computed properties, a getter’s result is cached based on its dependencies, and will only re-evaluate when some of its dependencies have changed.

Our Application

What we will build
While we are talking about Vuex and Vue js, it will be beneficial if we write a simple app that implements Vuex for its state management. To achieve this, we will be building a simple Todo List.

Below is a picture of what you will build:

Setting up the app
To get started, and also skip the process of configuring webpack for the compilation from ES6 to ES5, we will use the vue cli. If you do not have the Vue cli installed, install it by doing the following:

1sudo npm install -g vue-cli

After installing the vue-cli, it is now time for us to create a Vue project. To do that, we run the following command:

Note: For this tutorial, when running the command below, choose ‘no’ when asked to use Vue Router, as we will not be needing it.

1vue init webpack todo

After running the above code, we will need to change directory into our application, and install the required modules:

1//change directory to the folder
2      cd todo
3    //install all required npm modules
4      npm install

Installing and setting up Vuex

To install Vuex, we run the following command:

1npm install vuex

Once Vuex is installed, let’s explore our state, actions, mutations and getters for our sample application.

State:

1state: {
2      todos: [];
3    }

As mentioned earlier, the state is a JavaScript object containing the data we want to use in our store. In our example, our state is an object that consists of one key named todos. The todos key is an empty array which will hold all the todo items we have.

Actions:

1actions: {
2      ADD_TODO: function({ commit }, new_todo) {
3        var set_new = {
4          todo: new_todo,
5          status: false
6        };
7        commit("ADD_TODO_MUTATION", set_new);
8      },
9      COMPLETE_TODO: function({ commit }, todo) {
10        commit("COMPLETE_TODO_MUTATION", todo);
11      }
12    }

In our Actions section, we define two functions:

  • ADD_TODO: This function creates a new todo object with two keys : todo and status. The status key is set to ‘false’ by default as we want to use this field to check if a todo is complete or not. If the status field is true, then it means the todo item is complete. This function further commits a mutation called ADD_TODO_MUTATION with the new todo object it has created.
  • COMPLETE_TODO: This function sets a todo item to complete. It triggers a mutation called COMPLETE_TODO_MUTATION which in turn mutates the state of the todo object and sets it to ‘true’.

Mutations

1mutations: {
2      ADD_TODO_MUTATION: function(state, new_todo) {
3        state.todos.push(new_todo);
4      },
5      COMPLETE_TODO_MUTATION: function(state, todo) {
6        state.todos.find(x => x.todo === todo).status = true;
7      }
8    }

In the above code, we have our mutations, which directly alter the state of our store. Taking a closer look at the code, we notice two functions, which are:

  • ADD_TODO_MUTATION : This function pushes a new todo object to the array of todos we have in our store. Notice that the todo object added to our todos array is the exact todo that is being passed from our ADD_TODO action.
  • COMPLETE_TODO_MUTATION: This function receives a todo name, then looks for the exact todo object and sets its status to ‘true’. This implies that the selected todo is complete.

Getters

1getters: {
2      not_done: state => {
3        var filtered = state.todos.filter(function(el) {
4          return el.status === false;
5        });
6        return filtered;
7      },
8      done: state => {
9        var filtered = state.todos.filter(function(el) {
10          return el.status === true;
11        });
12        return filtered;
13      }
14    }

In our getters block, we define two functions:

  • not_done: This is a helper function which filters all our todos whose status is set to ‘false’.
  • done: This is a helper function which filters all our todos whose status is set to true.

Bringing our concepts together

Now that we have a working knowledge of what our state, actions, mutations, and getters are, let us bring them all together and create our Vuex store.

Let’s replace our store.js with the following content:

1// src/store.js
2
3    import Vue from "vue";
4    import Vuex from "vuex";
5    Vue.use(Vuex);
6    const store = new Vuex.Store({
7      state: {
8        todos: []
9      },
10      actions: {
11        ADD_TODO: function({ commit }, new_todo) {
12          var set_new = {
13            todo: new_todo,
14            status: false
15          };
16          commit("ADD_TODO_MUTATION", set_new);
17        },
18        COMPLETE_TODO: function({ commit }, todo) {
19          commit("COMPLETE_TODO_MUTATION", todo);
20        }
21      },
22      mutations: {
23        ADD_TODO_MUTATION: function(state, new_todo) {
24          state.todos.push(new_todo);
25        },
26        COMPLETE_TODO_MUTATION: function(state, todo) {
27          state.todos.find(x => x.todo === todo).status = true;
28        }
29      },
30      getters: {
31        not_done: state => {
32          var filtered = state.todos.filter(function(el) {
33            return el.status === false;
34          });
35          return filtered;
36        },
37        done: state => {
38          var filtered = state.todos.filter(function(el) {
39            return el.status === true;
40          });
41          return filtered;
42        }
43      }
44    });
45
46    export default store;

In the above code, we will notice that in the actions block, we have two functions. The first one commits the ADD_TODO_MUTATION, which pushes a new todo to the array of todos, while the second function commits the COMPLETE_TODO_MUTATION, which marks a todo as completed. These functions do not alter the state of our store.

If you remember, we said mutations are committed, this explains why we have the commit parameter passed to the actions by default.

Next, we move to the mutations block. Here, we also have two functions that alter the state of our store, and as such, the state parameter gets passed on to the functions.

We move to the getters block, where we define two helper functions, which we use to get completed `todos’, and those that are not.

Now, we have our store set up, but we are yet to see it work with our application or even integrate with our current application. Let’s move further and integrate the store.

Integrating the store with Vue

To notify Vue about our central store, we need to pass in the store object to our Vue instance while creating it. To do that, Let us replace the content of our src/main.js with the following:

1// src/main.js
2    // The Vue build version to load with the `import` command
3    with an alias.
4    import Vue from 'vue'
5    import App from './App'
6    import store from './store'
7    Vue.config.productionTip = false
8
9    /* eslint-disable no-new */
10    new Vue({
11      el: '#app',
12      template: '<App/>',
13      store,
14      components: { App }
15    })

In the above code block, we will notice the import of our store, and also that we pass the store into the object while initializing it. I am pretty sure you know that we used ES2016 syntax and used store as shorts for store: store.

Now, Vue is aware of our store, and it is now accessible as this.$store.
Next, Let us edit our hello world component, and make it more of a todo app.

Modifying our hello component

Let us open up our src\components\Hello.vue file and replace its content with the following:

1<template>
2
3      <div class="todolist not-done">
4        <h1>Todos</h1>
5        <input type="text" v-model="holder" class="form-control add-todo" placeholder="Add todo">
6        <button id="checkAll" class="btn btn-success" :disabled="holder==''" @click="add_todo">Add Todo</button>
7
8        <hr>
9        <ul id="sortable" class="list-unstyled" v-if="not_done_todos">
10          <li class="ui-state-default" v-for="todo in not_done_todos">
11            <div class="checkbox">
12              <label>
13                                    <input type="checkbox" @click="done_todo(todo.todo)"  :value="todo.todo" :checked="todo.status" />{{todo.todo}}</label>
14            </div>
15          </li>
16        </ul>
17        <div class="todo-footer" v-if="not_done_todos">
18          <strong><span class="count-todos">{{not_done_todos.length}}</span></strong> Item(s) Left
19        </div>
20      </div>
21    </template>
22
23    <script>
24    export default {
25      name: 'hello',
26      data () {
27        return {
28          holder: ''
29        }
30      }, 
31      methods: {
32        add_todo: function(){
33          this.$store.dispatch('ADD_TODO', this.holder);
34          this.holder = '';
35        },
36        done_todo: function(todo){
37          this.$store.dispatch('COMPLETE_TODO', todo);
38        }
39      },
40      computed: {
41        not_done_todos: function(){
42          return this.$store.getters.not_done;
43        }
44      }
45    }
46    </script>

In the code block above, you will notice that we have declared some set of HTML elements, in which we can find:

  • A text input which holds a new todo item.
  • A button element, to which we have attached a function, that adds a new todo item.
  • UL tags, coupled with LI tags for displaying uncompleted todos.
  • A check box element, which we check when completing a todo item.

We are now done reviewing the HTML part of our component.

Let us review our methods and computed properties, so we can see how we interact with Vuex.
Within our methods, we will notice the presence of two functions which are:

  • add_todo: This method dispatches the current todo item to the action called ADD_TODO. This action then commits the ADD_TODO_MUTATION.
  • done_todo: This method notifies our store about a completed todo item. By dispatching the action called COMPLETE_TODO with the name of the completed todo item, the store gets notified. This, in turn, commits the COMPLETE_TODO_MUTATION.

Moving into our computed properties, we will notice that we have only one function called not_done_todos. This function returns all the todos in our store which are not marked as complete. This is done by accessing what we call the getters. (If you remember, we defined some getters in our store which return filtered versions of our todos)

The reason why we access the getters as a computed property is so that our component can get the current state once the state gets altered.

Next, let us create another component to display a list of all the completed todos.

Creating our done component

Let us create a new component called Done.vue. We’ll copy the following content into it:

1<template>
2        <div class="done">
3             <div class="todolist">
4                 <h1>Already Done</h1>
5                    <ul id="done-items" class="list-unstyled" v-if="done_todos">
6                        <li v-for="todo in done_todos">{{todo.todo}}</li>
7
8                    </ul>
9                </div>
10        </div>
11    </template>
12    <script>
13        export default {
14            computed: {
15                done_todos: function() {
16                    return this.$store.getters.done;
17                }
18            }
19        }
20    </script>

In the above code block, we have created a list that shows all completed todos. By accessing a getter called done_todos, we get a filtered list of all completed todos in our store.

Finishing up the application

So far, we have created two components which both interact with the same store.
Now we will open up our App.vue file, and replace its content with the following:

1<template>
2      <div id="app"> 
3        <div class="container">
4        <div class="row">
5            <div class="col-md-6">
6                <hello></hello>
7            </div>
8            <div class="col-md-6">
9               <done></done>
10            </div>
11        </div>
12    </div>
13      </div>
14    </template>
15
16    <script>
17    import Hello from './components/Hello'
18    import Done from './components/Done'
19
20    export default {
21      name: 'app',
22      components: {
23        Hello,
24        Done
25      }
26    }
27    </script>

In the code above, we have imported the two components which we have used in the course of this exercise. Also, we have displayed the components side by side, so we can see how they work with the Vuex store.

Below is an image of what we have built:

Conclusion

In the course of this tutorial, we have covered what Vuex is, a use case scenario where Vuex might be useful, and how to use Vuex.
We have also learned about the core concepts of Vuex such as state, actions, mutations, and getter.
The codebase to this guide can be found here. Feel free to download and play around with the code.