Vue is awesome. But, like every other component based framework, it is difficult to keep track of state when the 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
Table of Contents
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/uuid@latest"></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.
Like this article? Follow @codebeast on Twitter