State Management in Vue: Getting Started with Vuex

0
1874

Vue is awesome. But, like every other component based framework, it is difficult to keep track of state when their your application starts growing. This difficulty is pronounced when their is so much data moving around from one component to another.

React pioneered the abstraction of state management using the Flux pattern. The Flux pattern was made obvious by Redux which is not dependent on the React library itself. This abstraction led other component based framework to implement a Flux/Redux solution that serves right the framework right. Vue introduced Vuex for this purpose and in this article, we will get our hands dirty with Vuex.

#The Challenge of State Management

Using state management solutions are not always required. When your app is small or does not have so much data flow, there is less need to employ Vuex. Otherwise, assuming we have a comment app with the following component structure:

This is still fairly simple yet, if we need to move data from the App component down to CommentItem component, we have to send down to CommentList first before CommentItem. It’s even more tedious when raising events from CommentItem to be handled by the App component.

It gets worse when you try to another another child to CommentItem. Probably a CommentButton component. This means when the button is clicked, you have to tell CommentItem, which will tell CommentList, and finally App. Trying to illustrate this already gives me headache, then consider when you have to implement it in a real project.

You might be tempted to keep local states for every component where that could work. The problem is, you will easily loose track of what exists and why a particular event is happening at a given time. It is easy to loose data sync which ends you up in a pool of confusion.

Vuex To The Rescue

You can lift the heavy duty of managing state from Vue to Vuex. Data flow and updates will be happening from one source and you end up wrapping your mind around one store to keep track of what is happening in your app.

#States! States!! States!!!

In the Flux/Redux/Vuex word, the word “state(s)” gets thrown around a lot and can get annoying because they mean different thing in different context. State basically means the current working status of an app. It is determined by what data exists and where such data exist at a given time.

In Vuex, states are just plain objects:

{
    counter: 0,
    list: [],
    // etc
}

Might look simple, but what you see above can be so powerful that it controls what happens in your application and why it happens. Before we dig into seeing more state magic, lets setup Vuex in a Vue project. Vue is simple to setup and we can just play on Codepen.

#Setup Vue and Vuex

Assuming you have an existing Vue starter app that looks like the example below:

const App = new Vue({
  template: `
    <div>Hello</div>
  `
});
App.$mount('#app');

When working with Vue and Vuex via script imports, Vuex automatically sets itself up. If using the module system, then you need to install Vuex:

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

Vue.use(Vuex)

Vuex Store

The Vuex store is a centralized means for managing state. Everything about the state including how to retrieve state values and update state values are defined in the store. You can create a store by creating an instance of Vuex’s Store and passing in our store details as object:

const store = new Vuex.Store({
  state: {
    count: 0
  }
})

The state we discussed previously is passed in alongside other store members that we will discuss in following sections.

In order to have access to this store via the Vue instance, you need to tell Vue about it:

const App = new Vue({
  template: `
    <div>Hello</div>
  `,
  // Configure store
  store: store
});
App.$mount('#app');

You can log this to the console via the created lifecycle hook to see that $store is now available:

#Rendering Store State

Vuex state is reactive. Therefore, you can treat it like the usual reactive object returned from a data function. We can use the computed member property to retrieve values:

...
var App = new Vue({
  computed: {
    counter: function() {
      return this.$store.state.counter
    }
  },
  template: `
    <p class="counter">{{counter}}</p>
  `,
  store: store
});

The computed counter can then be bound to the template using interpolation.

#Store Getters

We will violate DRY when more than one component depends on a computed value because we will have to compute for each component. Therefore, it’s best to handle computation inside the store using the getters property:

const store = new Vuex.Store({
  state: {
    counter: 0
  },
  getters: {
    counter: state => state.counter * 2
  }
})

var App = new Vue({
  computed: {
    counter: function() {
      return this.$store.getters.counter
    }
  },
  template: `
    <p class="counter">{{counter}}</p>
  `,
  store: store
});

#Store Mutations

Mutations are synchronous functions that are used to update state. State must not be updated directly, which means this is incorrect:

increment () {
    this.$store.state.counte++
}

The state changes must be done via mutation functions in the store:

const store = new Vuex.Store({
  state: {
    counter: 0
  },
  ...
  mutations: {
    // Mutations
    increment: state => state.counter++
  }
})

Mutations are like events. To call them, you have to use the commit method:

  methods: {
    increment: function () {
      this.$store.commit('increment')
    },
  },
  template: `
    <div>
      <p class="counter">{{counter}}</p>
      <div class="actions">
        <div class="actions-inner">
          <button @click="increment">+</button>
        </div>
      </div>
    </div>
  `,

#Todo With Vuex

The counter example is pretty basic and doesn’t have much use of Vuex. It’d be nice to take this to another level and try to build a todo app where we can create, complete, and remove todos.

Listing Todos

First thing, we need to make a list of todos. This list will live in our store’s state as array:

const store = new Vuex.Store({
  state: {
    todos: [
      {
        task: 'Code',
        completed: true
      },
      {
        task: 'Sleep',
        completed: false
      },
      {
        task: 'Eat',
        completed: false
      }
    ]
  },
})

We can render this list of todos in our component:

const store = new Vuex.Store({
  state: {
    todos: [
      ...
    ]
  },
  getters: {
    todos: state => state.todos
  }
})

const TodoList = {
  props: ['todos'],
  template: `
    <div>
      <ul>
        <li v-for="t in todos" :class="{completed: t.completed}">{{t.task}}</li>
      </ul>
    </div>
  `
}

var App = new Vue({
  computed: {
    todos: function() {
      return this.$store.getters.todos
    }
  },
  template: `
    <div>
      <todo-list :todos="todos"></todo-list>
    </div>
  `,
  store: store,
  components: {
    // Add child component to App
    'todo-list': TodoList
  }
});

Rather than write all our logic in one App component, we wrote todo list relating logic in a different component, TodoList. App needs to be aware of this new component, therefore, we import the component and render it:

template: `
    ...
      <todo-list :todos="todos"></todo-list>
    ...
  `,
components: {
    'todo-list': TodoList
  }

TodoList receives the list of todos via props, iterates of the the todos, strikes of completed todos, and renders each of the todo items.

Creating New Todos

To create new todos, we need a text box where the task can be entered. The text will be wrapped with a form:

// Store
const store = new Vuex.Store({
  state: {
    todos: [
      ...
    ]
  },
  ...
  mutations: {
    // Add todo mutation
    addTodo: (state, payload) => {
      // Assemble data
      const task = {
        task: payload,
        completed: false
      }
      // Add to existing todos
      state.todos.unshift(task);
    }
  }
})

// App Component
var App = new Vue({
  data: function() {
    return {
      task: ''
    }
  },
  methods: {
    addTodo: function() {
      // Commit to mutation
      this.$store.commit('addTodo', this.task)
      // Empty text input
      this.task = ''
    }
  },
  template: `
    <div>
      <form @submit.prevent="addTodo">
        <input type="text" v-model="task" />
      </form>
      <todo-list :todos="todos"></todo-list>
    </div>
  `,
  ...
});

Completing and Removing Todos

To complete a todo, we need to find what todo needs to be completed and toggle it’s completed property. This can be triggered by clicking on each item on the todo list:

const store = new Vuex.Store({
  state: {
    todos: [
      ...
    ]
  },
  mutations: {
    addTodo: (state, payload) => {
      const task = {
        task: payload,
        completed: false,
        id: uuid.v4()
      }
      console.log(state)
      state.todos.unshift(task);
    },
    // Toggle Todo
    toggleTodo: (state, payload) => {
      state.todos = state.todos.map(t => {
        if(t.id === payload) {
          // Update the todo 
          // that matches the clicked item
          return {task: t.task, completed: !t.completed, id: t.id}
        }
        return t;
      })
    }
  }
})

const todoList = {
  props: ['todos'],
  methods: {
    toggleTodo: function(id) {
      this.$store.commit('toggleTodo', id)
    }
  },
  template: `
    <div>
      <ul>
        <li v-for="t in todos" :class="{completed: t.completed}" @click="toggleTodo(t.id)">{{t.task}}</li>
      </ul>
    </div>
  `,
}

When a todo item is clicked, we raise a toggleTodo event. This event’s handler receives an parameter id and commits a toggleTodo mutation with the id.

In the mutation function, we iterate over each of the items, find the one that needs to be update, and update it.

We added a uuid library in other to uniquely identify each items when updating them:

<script src="http://wzrd.in/standalone/[email protected]"></script>

You could choose to remove todo items entirely from the list. We can do this by listening to dblclick event:

const store = new Vuex.Store({
  state: {
    todos: [
      ...
    ]
  },
  getters: {
    todos: state => state.todos
  },
  mutations: {
    deleteTodo: (state, payload) => {  
      const index = state.todos.findIndex(t => t.id === payload);
      state.todos.splice(index, 1) 
      console.log(index)
    }
  }
})

const todoList = {
  props: ['todos'],
  methods: {
    toggleTodo: function(id) {
      this.$store.commit('toggleTodo', id)
    },
    deleteTodo: function(id) {
      this.$store.commit('deleteTodo', id)
    }
  },
  template: `
    <div>
      <ul>
        <li v-for="t in todos" :class="{completed: t.completed}" @click="toggleTodo(t.id)" @dblclick="deleteTodo(t.id)">{{t.task}}</li>
      </ul>
    </div>
  `,
}

Similar to what we did for toggling todos. We just remove an item from the array based on the id of the todo item matched.

#Final Notes

We built a basic counter and fairly practical todo app to showcase how you can use Vuex in common state manipulation tasks.

It would be nice to keep one thing in mind before we close off. Mutations are synchronous but the real world is not. Therefore, we need a way to handle asynchronous actions. Vuex provides another member called Actions. Actions allows you to carry out async operations and then commit mutations when the async operations are completed.

Suggest

Angular, React.js & Vue.js – Quickstart & Comparison

Vue JS 2 – The Complete Guide (incl. Vuex)

Learn by Doing: Vue JS 2.0 the Right Way

Source viva: https://scotch.io/tutorials/state-management-in-vue-getting-started-with-vue

LEAVE A REPLY

Please enter your comment!
Please enter your name here