At some point when building applications you will need to begin thinking about how to manage state. Vue provides a simple way to work with local state inside a component. Below is an example of loading some data from an api and setting it on the local state variable projects:

import axios from 'axios' export default {

name: 'projects',

data: function () {

return {

projects: []

}

},

methods: {

loadProjects: function () {

axios.get('/secured/projects').then((response) => {

this.projects = response.data

}, (err) => {

console.log(err)

})

}

},

mounted: function () {

this.loadProjects()

}

}

</script>

In our view template you can simply access the projects array and work with them in the template. This works great and is simple, but what happens if you had another component that needs to display the same project information? You could reload it from the server, but if updates are made to the data you would need to reload other components from the server as well. While this approach will work, it’s certainly not performant and creates repetitive code in your application. What is the answer? As with anything … it depends! One way to handle it is with Vuex, a centralized state management tool that is tightly integrated with Vue. There are other ways to approach this problem, but in the this post I am going to focus on vuex.

You can find the completed repo for the example I will build in this post here.

TL;DR — Vuex is an awesome tool for managing state in Vue applications. Jump to What’s the point at the end for an example of why it matters.

What is Vuex?

From the vuex documentation:

Vuex is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.

Here is a vuex store definition:

import Vue from 'vue'

import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({

state: { },

actions: { },

mutations: { },

getters: { },

modules: {



}

}) export default store

There are 5 objects that make up a store, let’s explore each one.

state:

This is where you define the data structure of your application. You can also set default or initial state here.

state: {

projects: [],

userProfile: {}

}

actions:

Actions are where you define the calls that will commit changes to your store. A common example of this would be a call to an api to retrieve data, once it completes you call store.commit() to invoke a mutation to actually update the data in the store. Actions are called in your components via dispatch call.

actions: {

LOAD_PROJECT_LIST: function ({ commit }) {

axios.get('/secured/projects').then((response) => {

commit('SET_PROJECT_LIST', { list: response.data })

}, (err) => {

console.log(err)

})

}

}

mutations:

The mutations calls are the only place that the store can be updated.

mutations: {

SET_PROJECT_LIST: (state, { list }) => {

state.projects = list

}

}

getters:

Getters are a way to grab computed data from the store. For example, if you have a project list, one component might only want to show projects that are completed:

getters: {

completedProjects: state => {

return state.projects.filter(project => project.completed).length

}

}

modules:

The modules object provides a way to split your store in multiple stores, but allow them to all remain part of the store tree. As your application grows this is a good way to organize your codebase. More details on modules can be found in the vuex documentation here.

Example

Now that we understand the basics of vuex, let’s create an actual app that will use it. I am going to be building on top of an app from a previous post, if you need details on how we got to this point you can find the them here. The app is using Auth0 for security, if you don’t want to set that up, just remove the beforeEnter calls in the router to remove the security guard. Also, in the api, remove the /secured from the routes.

In order to get things up and running, we need to setup the vuex store. First, add vuex to the project:

$ yarn add vuex

Once this completes, create a directory in the root of src named store and add an index.js file in the directory. To get started, just drop in the basic store definition:

import Vue from 'vue'

import Vuex from 'vuex'

import axios from 'axios' Vue.use(Vuex) const store = new Vuex.Store({

state: {



},

actions: {



},

mutations: {



},

getters: {



}

}) export default store

Back in the main.js file import the store and add it to the root vue object:

import store from './store' /* eslint-disable no-new */

new Vue({

template: `

<div>

<navbar />

<section class="section">

<div class="container is-fluid">

<router-view></router-view>

</div>

</section>

</div>

`,

router,

store,

components: {

navbar

}

}).$mount('#app')

Next, setup the vuex store (/src/store/index.js):

import Vue from 'vue'

import Vuex from 'vuex'

import axios from 'axios' Vue.use(Vuex) const store = new Vuex.Store({

state: {

projects: []

},

actions: {

LOAD_PROJECT_LIST: function ({ commit }) {

axios.get('/secured/projects').then((response) => {

commit('SET_PROJECT_LIST', { list: response.data })

}, (err) => {

console.log(err)

})

}

},

mutations: {

SET_PROJECT_LIST: (state, { list }) => {

state.projects = list

}

},

getters: {

openProjects: state => {

return state.projects.filter(project => !project.completed)

}

}

}) export default store

I defined the projects array in the state, created an action to go get the list of the projects and commit them to allow the mutation function to set the values. I also have a getter to return only completed projects. Now that the store is setup, I will refactor the projects page into a few components. First create a projectList component to render a table of the list.

/src/components/projectList.vue:

<template lang="html">

<div class="">

<table class="table">

<thead>

<tr>

<th>Project Name</th>

<th>Assigned To</th>

<th>Priority</th>

<th>Completed</th>

</tr>

</thead>

<tbody>

<tr v-for="item in projects">

<td>{{item.name}}</td>

<td>{{item.assignedTo}}</td>

<td>{{item.priority}}</td>

<td><i v-if="item.completed" class="fa fa-check"></i></td>

</tr>

</tbody>

</table>

</div>

</template> <script>

import { mapState } from 'vuex' export default {

name: 'projectList',

computed: mapState([

'projects'

])

}

</script> <style lang="css">

</style>

The template is pretty straight forward, we access the store information thru a computed object. One important thing to note is the use of the helper function mapState. When adding the objects to the computed object from the state, you could write out the full path:

computed: {

projects () {

return this.$store.state.projects

}

}

mapState is a helper function provided by vuex to simplify the creation of these objects. The end result is exactly the same, feel free to use whatever is more comfortable.

Back in the project.vue container, import the projectList component and add it to the definition. Notice the call this.$store.dispatch('LOAD_PROJECT_LIST) which will load the projects into the store from the server. When thinking about where to load state you always want to “lift the state up” as far as it makes sense. In our case, the top level project container should load the projects into the store.

<template lang="html">

<div id="projects">

<div class="columns">

<div class="column is-half">

<div class="notification">

Project List

</div>

<project-list />

</div>

</div>

</div>

</template> <script>

import projectList from '../components/projectList' export default {

name: 'projects',

components: {

projectList

},

mounted: function () {

this.$store.dispatch('LOAD_PROJECT_LIST')

}

}

</script>

If you run the application, the project list will be rendered from the vuex state container. Now that the list of projects is displayed, it’s time to setup a way to add a new project.

Back in the vuex store, add a new action and mutation to handle getting the new project into the store:

// under actions:

ADD_NEW_PROJECT: function ({ commit }) {

axios.post('/secured/projects').then((response) => {

commit('ADD_PROJECT', { project: response.data })

}, (err) => {

console.log(err)

})

} // under mutations:

ADD_PROJECT: (state, { project }) => {

state.projects.push(project)

}

***One note on the server side of this call, I have just created a simple array on the server and a post method to add a new project to the array and send it back. You can see the details in the /server/app.js file in the project.

Next, create a simple component under the components folder named addProject.vue:



<button type="button" class="button"

</template> @click ="addProject()">Add New Project <script>

export default {

name: 'addProject',

methods: {

addProject () {

this.$store.dispatch('ADD_NEW_PROJECT')

}

}

}

</script>

This component dispatches an action to add a new project to the list. Import the addProject component into the projects.vue container and add it in the template.

<template lang="html">

<div id="projects">

<div class="columns">

<div class="column is-half">

<div class="notification">

Project List

</div>

<project-list />

<add-project />

</div>

</div>

</div>

</template> <script>

import projectList from '../components/projectList'

import addProject from '../components/addProject' export default {

name: 'projects',

components: {

projectList,

addProject

},

mounted: function () {

this.$store.dispatch('LOAD_PROJECT_LIST')

}

}

</script>

Run the app and you will see when the server returns the new project successfully it will get committed to the store and displayed on the screen. I am going to add one additional component to toggle the completed flag on a given project. Create a new component named completeToggle.vue:



<button type="button" class="button"

<i class="fa fa-undo" v-if="item.completed"></i>

<i class="fa fa-check-circle" v-else></i>

</button>

</template> @click ="toggle(item)"> <script>

export default {

name: 'completeToggle',

props: ['item'],

methods: {

toggle (item) {

this.$store.dispatch('TOGGLE_COMPLETED', { item: item })

}

}

}

</script>

This component will show a button for each project that will call a toggle function to complete and un-complete a project. I am passing the item in via props and the toggle method will dispatch the action TOGGLE_COMPLETED, which will update the value on the server and send it back. Once the update is complete, the commit function is called to update the state with the new data. Here are the new functions for the store:

// actions

TOGGLE_COMPLETED: function ({ commit, state }, { item }) {

axios.put('/secured/projects/' + item.id, item).then((response) => {

commit('UPDATE_PROJECT', { item: response.data })

}, (err) => {

console.log(err)

})

} // mutations

UPDATE_PROJECT: (state, { item }) => {

let idx = state.projects.map(p => p.id).indexOf(item.id)

state.projects.splice(idx, 1, item)

}

The UPDATE_PROJECT mutation is removing the row from the array and adding back the new row from the server. This way, if another actions updates a different part of the object I can still use this mutation to update the store. For full details of the server call, be sure to check out the server/app.js file in the github repo. Here is the put call on the server:

app.put('/secured/projects/:id', function (req, res) {

let project = data.filter(function (p) { return p.id == req.params.id })

if (project.length > 0) {

project[0].completed = !project[0].completed

res.status(201).json(project[0])

} else {

res.sendStatus(404)

}

})

Last step is to import the completeToggle component into the projectList component and add it to the table, pass the item to completeToggle.

// new column in table

<td><complete-toggle :item="item" /></td> // be sure import and add to the components object

Now when you run the app you can add new projects and complete them. The data is saved in the vuex store and on the server side (in memory).

What’s the point?

So now the app has some basic features, but you might be asking yourself the same question I was when I first started exploring this tool … is it really worth all the trouble? It seems to over complicate the simple function of adding a row and toggling a field on a project list. I would agree that if what we have completed so far was the total requirement for your app, then yes this was over kill. But what if you needed another component to display some summary information about the project list? Would you fetch the data from the server every time you made a change? Emit some type of event to keep it updated? Let’s see how vuex solves this problem for us. Add another component named projectStatus.vue.

<template lang="html">

<article class="message">

<div class="message-header">

<p>Project Status:</p>

</div>

<div class="message-body">

<div class="control">

<span class="tag is-info">Number of projects: {{projectCount}}</span>

</div>

<div class="control">

<span class="tag is-success">Completed: {{completedProjects}}</span>

</div>

</div>

</article>

</template> <script>

import { mapGetters } from 'vuex' export default {

name: 'projectStatus',

computed: {

...mapGetters([

'completedProjects',

'projectCount'

])

}

}

</script>

This component will show us the total number of projects and how many are completed. I am using the mapGetters helper function to shorten the definition and these functions are defined in the store:

getters: {

completedProjects: state => {

return state.projects.filter(project =>project.completed).length

},

projectCount: state => {

return state.projects.length

}

}

Now add this component to the projectList page next the table and you will see that as you add new projects and complete them, the numbers will automatically update for you. This component can be used anywhere in the app and keep your users updated on the status.

Conclusion

Even though this is a simple example, it helps understand how managing state can be efficient. Use common sense when applying it, sometimes components are so simple they don’t need it, but as your application grows it starts to make sense to use a tool to aid in state management.