In this tutorial we'll be learning how to use Vuex in our VueJs projects by building a notes application. We'll briefly go over what Vuex is, when to use it, and how to structure your project for use with a Vuex application. We will then apply those concepts to the notes app we're building, step-by-step.

Here's a screenshot of the notes application we'll be building:

VueJs Notes Application with Vuex

You can download the completed code from the GitHub Repo. Also be sure to check out the demo to have an idea of what we'll be building throughout this tutorial.

Overview of Vuex

Before we jump into building the notes app right away, I want to take a couple of minutes to go over some of the core concepts of Vuex. (I'll try to stay clear of all the fancy buzz words, but when I do use any, I'll be sure to explain them)

Vuex is a library that enforces a Flux-like application architecture to help you build medium-to-large-scale SPAs (Single Page Applications). It gives you a way to structure those applications and manage their state in a maintainable and easy-to-reason-about way.

The word state may seem ambiguous the first time you hear it, so to put it simply, think of state as just data you use in your Vue apps. However, Vuex makes a distinction between component local state and application level state:

  • component local state: state that is used by only one component (think of the data option you pass to a Vue component)
  • application level state: state that is used by more than one component

So to put this into context: say you have a parent component and this parent has 2 child components. The parent can easily send data to the child components using props, so we've got that channel of communication covered.

Now what about when the 2 siblings want to communicate with one another because they both need the same piece of data. Or when a child needs to pass data to its parent? This shouldn't be too hard when your application is small because you can get away with using shared event emitters to communicate between parent and child.

However, as your application grows:

  • Keeping track of all these events becomes difficult. Which component is triggering the event? Who is listening?
  • The business logic is spread across many different components as opposed to being centralized - leading to unintended side-effects
  • The parents become more tightly coupled to their children since they have to explicitly dispatch and listen to certain events

These are some of the issues Vuex solves. The 4 core concepts behind the Vuex system are:

  • The State Tree: an object that contains all the application level state
  • Getters: used access data in the store from within our Vue components
  • Mutators: event handlers that manipulate the state
  • Actions: functions called by the Vue components to dispatch the mutations

If you don't fully understand what those 4 terms mean, don't worry! We'll be going through them in more detail as we build our application.

The following diagram does a great job at depicting the way data flows in a Vuex application (from the Vuex docs):

Vuex Data Flow

Some important things to note about this diagram:

  • The data flow is unidirectional
  • Components can call actions
  • Actions are used to dispatch mutations
  • Only mutations can mutate the state (components can't/shouldn't mutate the state directly)
  • The store is reactive - whenever the state is updated, the components will reflect those changes

Setting Up the Project

Now that we have some of the basic Vuex concepts covered, let's set up the project directory structure which will look like this by the time we're done:

Vuex Project Structure

  • components/ will contain our VueJs components which we will be building in the later sections
  • vuex/ contains the files related to our Vuex store (state object, actions, mutators)
  • build.js is the output bundle from Webpack
  • index.html is the page we will include our root Vue component and Webpack bundle in
  • main.js is the entry point for our application which contains the root Vue instance
  • styles.css has some basic CSS to give our app some life
  • webpack.config.js contains the Webpack configuration which we will dissect in a minute

Go ahead and create an empty directory, cd into it, and initialize a package.json:

mkdir vuex-notes-app && cd vuex-notes-app
npm init -y

Now let's install all our dependencies starting off with our dev-dependencies for Webpack + vue-loader:

npm install\
  webpack webpack-dev-server\
  vue-loader vue-html-loader css-loader vue-style-loader vue-hot-reload-api\
  babel-loader babel-core babel-plugin-transform-runtime babel-preset-es2015\
  [email protected]5\
  --save-dev

and install Vue and Vuex:

npm install vue vuex --save

With all the dependencies installed we can configure Webpack. Go ahead and create the webpack.config.js in the root of our project directory which will look like so:

module.exports = {
  entry: './main.js',
  output: {
    path: __dirname,
    filename: 'build.js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        loader: 'babel',
        exclude: /node_modules/
      },
      {
        test: /\.vue$/,
        loader: 'vue'
      }
    ]
  },
  babel: {
    presets: ['es2015'],
    plugins: ['transform-runtime']
  }
}

Let's quickly break down what some of these Webpack configurations mean. We're specifying the entry point to main.js which is going to contain our root Vue instance. From there Webpack will traverse all our imports and take care of resolving the dependencies for us. Once the project has been bundled, Webpack will spit out the bundled code to the build.js file we saw in the directory structure above. That's basically all the output option is saying.

Now onto the usual loaders which simply tell Webpack what to do when it encounters a specific type of file. In our case we want to run 2 loaders: the vue-loader for handling the .vue files and the babel-loader to transform the ES2015 code we write into ES5 code understood by the browsers.

Finally we configure babel to use the es2015-preset which we installed earlier since we will be writing code in ES2015.

The last step is to edit the NPM scripts so that we can run Webpack dev server with HMR (Hot Module Replacement) and be able to bundle and minify the code for production. Update the scripts object in the package.json to look like this:

"scripts": {
  "dev": "webpack-dev-server --inline --hot",
  "build": "webpack -p"
}

Now we can run the dev server with npm run dev and we can bundle and minify the app with npm run build.

Creating the Vuex Store

In this section we will be taking a deeper look into the Vuex store that we'll be creating for our notes application. The store acts like a container for our application's state so let's start off with the basic skeleton of what our store will look like. Create a store.js file in the vuex/ folder:

// store.js

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

Vue.use(Vuex)

// the root, initial state object
const state = {
  notes: [],
  activeNote: {}
}

// define the possible mutations that can be applied to our state
const mutations = {

}

// create the Vuex instance by combining the state and mutations objects
// then export the Vuex store for use by our components
export default new Vuex.Store({
  state,
  mutations
})

It's always recommended (and good practice) to initialize the state.

I'll go through the skeleton above and explain it in the context of our notes application. Here's a visual breakdown of our components (which we will implement in the next section):

Vuex Notes App Components

  • App, the root Vue component, is the outer red box
  • Toolbar is the green box to the left with the add, favorite, and delete buttons
  • NotesList is the purple box which contains the list of notes that a user can select from. It also allows the user to toggle between all their notes and just the ones they've starred
  • Editor is the yellow box to the right which will display the selected note's contents

The state object in store.js will contain the application level state, which if you recall from the previous section, is any state that is shared between multiple components.

The list of notes (notes: []) holds our notes objects that the NotesList component will render. The active note (activeNote: {}) will hold the currently selected note object which multiple components will need to be aware of:

  • The Toolbar component to star and delete the currently selected note
  • The NotesList component to highlight the selected note in the list by manipulating the CSS
  • The Editor component to display the active notes content

Moving on to the mutations object. Think of mutations as the only means by which you can modify the state. The mutators we will be implementing for our notes app are:

  • adding a note to our notes array (state.notes)
  • setting the active note (state.activeNote) to the one selected by the user from the list of notes
  • deleting the active note
  • editing the active note
  • toggling between favorite/unfavorite for the active note

Remember, components cannot modify the state directly, they must call an action which dispatches a mutation. With that being said, let's go ahead and implement the basic mutators listed above.

Mutators always receive the state tree as the first argument followed by any number of additional arguments you wish to pass to it, referred to as payload arguments.

When a user wants to add a new note, we want to:

  • create a new object
  • initialize its properties
  • push it to the notes array
  • set the activeNote to the new note we just created
ADD_NOTE (state) {
  const newNote = {
    text: 'New note',
    favorite: false
  }
  // only mutators can mutate the state
  state.notes.push(newNote)
  state.activeNote = newNote
}

It's convention to write mutators in uppercase to help distinguish them from plain functions.

Editing a note takes in the text that the user enters and updates the currently active note's text to the text received as a payload:

EDIT_NOTE (state, text) {
  state.activeNote.text = text
}

As you can see, our mutators are rather simple and there isn't much to them. The key thing to take away from this is that any modifications to the state must be made in a mutator and a mutation is dispatched by an action.

The remaining mutators are quite straight forward so I'll avoid explaining each and every one of them because they all follow the same basic idea above. Our entire vuex/store.js file should look like so:

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

Vue.use(Vuex)

const state = {
  notes: [],
  activeNote: {}
}

const mutations = {
  ADD_NOTE (state) {
    const newNote = {
      text: 'New note',
      favorite: false
    }
    state.notes.push(newNote)
    state.activeNote = newNote
  },

  EDIT_NOTE (state, text) {
    state.activeNote.text = text
  },

  DELETE_NOTE (state) {
    state.notes.$remove(state.activeNote)
    state.activeNote = state.notes[0]
  },

  TOGGLE_FAVORITE (state) {
    state.activeNote.favorite = !state.activeNote.favorite
  },

  SET_ACTIVE_NOTE (state, note) {
    state.activeNote = note
  }
}

export default new Vuex.Store({
  state,
  mutations
})

Actions are just functions that can be called by our components to dispatch mutations. They receive the store as the first argument followed by any number of additional arguments.

For instance, when a user clicks on the add button in the Toolbar component, we want to call an action to dispatch the ADD_NOTE mutation. Let's create a file called actions.js in the vuex/ directory where we can create an addNote function to dispatch this mutation:

export const addNote = ({ dispatch }) => {
  dispatch('ADD_NOTE')
}

We're using ES2015 argument destructuring which you may or may not be familiar with. Alternatively, the above code can be written like so:

export const addNote = function (store) {
  var dispatch = store.dispatch
  dispatch('ADD_NOTE')
}

I'd encourage you to get familiar with ES2015, if you're not already, as it allows you to write much more elegant code once you get the hang of it.

The addNote action is quite simple and all it does is dispatch the ADD_NOTE mutation. We can now import the vuex/actions.js file in any component file and call actions to dispatch mutations.

You may be wondering, why bother calling an action to dispatch a mutation? Why don't we just call the mutation from within the component? The main reason for this is that mutations must be synchronous, whereas actions can be asynchronous. What this basically means is: if you want to make any AJAX requests, for instance, you would have to make them in the actions and not the mutations. The Vuex docs give a great example of why mutations must be synchronous while actions can be asynchronous.

The remaining actions follow the same logic and the entire actions.js file will look like so:

export const addNote = ({ dispatch }) => {
  dispatch('ADD_NOTE')
}

export const editNote = ({ dispatch }, e) => {
  dispatch('EDIT_NOTE', e.target.value)
}

export const deleteNote = ({ dispatch }) => {
  dispatch('DELETE_NOTE')
}

export const updateActiveNote = ({ dispatch }, note) => {
  dispatch('SET_ACTIVE_NOTE', note)
}

export const toggleFavorite = ({ dispatch }) => {
  dispatch('TOGGLE_FAVORITE')
}

That sums it up for all we'll be needing to do in our vuex/ directory. We've successfully managed to create the store.js file which will hold our state object and mutators. We also created actions.js which contained some simple functions to dispatch mutations.

Building the Vue Components

In this final section we'll be implementing the 4 components (App, Toolbar, NotesList, and Editor) and learning how to use the Vuex store from within these components to retrieve data and call the actions we created. Just as a reminder of the component breakdown, here's the diagram again:

Vuex Notes App Components

Creating the Root Instance - main.js

main.js will be our root Vue instance and the entry point for our application. For this reason we will import our Vuex store here and inject it to all it's children:

// main.js

import Vue from 'vue'
import store from './vuex/store'
import App from './components/App.vue'

new Vue({
  store, // inject store to all children
  el: 'body',
  components: { App }
})

We are also importing the root Vue component called App which we will be creating next.

App - The Root Component

The root App component will be importing and combining the other 3 components: Toolbar, NotesList, and Editor. To put this into code, our components/App.vue file will look like so:

<template>
  <div id="app">
    <toolbar></toolbar>
    <notes-list></notes-list>
    <editor></editor>
  </div>
</template>

<script>
import Toolbar from './Toolbar.vue'
import NotesList from './NotesList.vue'
import Editor from './Editor.vue'

export default {
  components: {
    Toolbar,
    NotesList,
    Editor
  }
}
</script>

We can include the App component in the index.html file along with the bundle from Webpack, Bootstrap for some basic styling, and the styles.css for our app which you can grab here:

<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Notes | coligo.io</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <app></app>
    <script src="build.js"></script>
  </body>
</html>

Toolbar

The Toolbar component gives the users 3 options: create a new note, favorite/unfavorite the currently selected note, and delete the currently selected note.

Toolbar Component in Vuex Notes App

This is a great use-case for Vuex because the Toolbar component needs to know which note is currently selected from the list of notes so that we can delete and favorite/unfavorite the selected note. However the currently selected note is in it's own component, NotesList.

This is where the activeNote property in our state object comes in handy. Whenever the user clicks on a note in the list, the NotesList component will call the updateActiveNote() action to dispatch the SET_ACTIVE_NOTE mutation with the newly selected note as the payload. The mutator will, in turn, update the activeNote property in the state object to point to the selected note.

With that being said, the Toolbar component needs to get the activeNote property from the state.

To access a Vuex store property from inside a component, we pass the vuex: {} option to the component with a getters object. A state getter takes the state as the first parameter and binds the return value to the component so you could access it just as you would with any data property defined on the component.

vuex: {
  getters: {
    activeNote: state => state.activeNote
  }
}

We also want to make it so that whenever a user clicks on any of the 3 buttons, we call the appropriate action. To do this we import actions.js and define them in the vuex.actions option:

import { addNote, deleteNote, toggleFavorite } from '../vuex/actions'

export default {
  vuex: {
    getters: {
      activeNote: state => state.activeNote
    },
    actions: {
      addNote,
      deleteNote,
      toggleFavorite
    }
  }
}

This will bind the actions to the component's store instance and allow us to call the addNote, deleteNote, and toggleFavorite methods the same way we would call component instance methods. Putting this all together our components/Toolbar.vue file will look like so:

<template>
  <div id="toolbar">
    <i @click="addNote" class="glyphicon glyphicon-plus"></i>
    <i @click="toggleFavorite"
      class="glyphicon glyphicon-star"
      :class="{starred: activeNote.favorite}"></i>
    <i @click="deleteNote" class="glyphicon glyphicon-remove"></i>
  </div>
</template>

<script>
import { addNote, deleteNote, toggleFavorite } from '../vuex/actions'

export default {
  vuex: {
    getters: {
      activeNote: state => state.activeNote
    },
    actions: {
      addNote,
      deleteNote,
      toggleFavorite
    }
  }
}
</script>

As you can see, we're calling the addNote() action the same way we would call a component's method:

<i @click="addNote" class="glyphicon glyphicon-plus"></i>

For the favorite button, we have a bit of additional logic. We want to add the starred class to the button if the selected note is favorited. This will simply give the icon a yellow color which will serve as a visual cue to the user that the note is in their favorites, like so:

Starred Note in Toolbar Component

NotesList

The NotesList component has 3 main roles:

  1. Rendering the notes as a list
  2. Allowing the user to filter the notes to show all them or just the favorites
  3. Calling the updateActiveNote action to update the activeNote in the store when the user selects a note from the list

From the above requirements we can see that we'll need both the notes array and the activeNote object from the store.

Let's go ahead and define the state getters for them:

vuex: {
  getters: {
    notes: state => state.notes,
    activeNote: state => state.activeNote
  }
}

The Vuex store is reactive so whenever the store updates, the components will be updated accordingly as well.

To call the updateActiveNote action when a user selects a note from the list (requirement 3), we will need to import the actions.js and define the updateActiveNote action in the vuex.actions option:

import { updateActiveNote } from '../vuex/actions'

export default {
  vuex: {
    getters: {
      notes: state => state.notes,
      activeNote: state => state.activeNote
    },
    actions: {
      updateActiveNote
    }
  }
}

Next, let's create a computed property that will return a list of filtered notes based on whether the user has selected 'All Notes' or 'Favorites':

import { updateActiveNote } from '../vuex/actions'

export default {
  data () {
    return {
      show: 'all'
    }
  },
  vuex: {
    getters: {
      notes: state => state.notes,
      activeNote: state => state.activeNote
    },
    actions: {
      updateActiveNote
    }
  },
  computed: {
    filteredNotes () {
      if (this.show === 'all'){
        return this.notes
      } else if (this.show === 'favorites') {
        return this.notes.filter(note => note.favorite)
      }
    }
  }
}

The show property in the data option is referred to as component local state since it is only used within the NotesList component. We will use show property to toggle between 'all' and 'favorites' when a user clicks on the buttons which will result in the computed property returning the appropriate set of notes.

The complete NotesList.vue component looks like so:

<template>
  <div id="notes-list">

    <div id="list-header">
      <h2>Notes | coligo</h2>
      <div class="btn-group btn-group-justified" role="group">
        <!-- All Notes button -->
        <div class="btn-group" role="group">
          <button type="button" class="btn btn-default"
            @click="show = 'all'"
            :class="{active: show === 'all'}">
            All Notes
          </button>
        </div>
        <!-- Favorites Button -->
        <div class="btn-group" role="group">
          <button type="button" class="btn btn-default"
            @click="show = 'favorites'"
            :class="{active: show === 'favorites'}">
            Favorites
          </button>
        </div>
      </div>
    </div>
    <!-- render notes in a list -->
    <div class="container">
      <div class="list-group">
        <a v-for="note in filteredNotes"
          class="list-group-item" href="#"
          :class="{active: activeNote === note}"
          @click="updateActiveNote(note)">
          <h4 class="list-group-item-heading">
            {{note.text.trim().substring(0, 30)}}
          </h4>
        </a>
      </div>
    </div>

  </div>
</template>

<script>
import { updateActiveNote } from '../vuex/actions'

export default {
  data () {
    return {
      show: 'all'
    }
  },
  vuex: {
    getters: {
      notes: state => state.notes,
      activeNote: state => state.activeNote
    },
    actions: {
      updateActiveNote
    }
  },
  computed: {
    filteredNotes () {
      if (this.show === 'all'){
        return this.notes
      } else if (this.show === 'favorites') {
        return this.notes.filter(note => note.favorite)
      }
    }
  }
}
</script>

A couple of interesting things to note about the template:

  • We are using the first 30 characters as the note's title in the list: note.text.trim().substring(0, 30)
  • When a user clicks on a note, the note is passed to the updateActiveNote(note) action to dispatch the SET_ACTIVE_NOTE mutation with the new note as a payload
  • Simple click handlers are used to set the show property to 'all' or 'favorite' when a filter button is clicked
  • We use the :class="" bindings to add the Bootstrap active class to the currently selected item in the list and to the currently selected filter ('All Notes' or 'Favorites')

Editor

The Editor component is the most simple one. It only does 2 things:

  • It grabs the currently activeNote from the store and displays it in a <textarea>
  • It calls the editNote() action whenever the user updates the note

To do this we will need to define a getter and an action in our component's vuex option:

import { editNote } from '../vuex/actions'

export default {
  vuex: {
    getters: {
      activeNoteText: state => state.activeNote.text
    },
    actions: {
      editNote
    }
  }
}

We can now bind the value of the <textarea> to the activeNoteText from the store and register the editNote action as the event handler that will be triggered on any input event by the user:

<div id="note-editor">
  <textarea
    :value="activeNoteText"
    @input="editNote"
    class="form-control">
  </textarea>
</div>

You might be wondering why we can't just use the v-model directive. If we did apply the v-model directive like so:

<textarea v-model="activeNoteText" class="form-control"></textarea>

then the v-model will attempt to directly mutate the activeNote from within the component which goes against the idea of calling actions to dispatch mutations, making it explicit and trackable. Also, if you are using strict mode, this will cause an error.

After all, the v-model directive is just syntax sugar for a :value binding and @input event handler.

Here's what the completed components/Editor.vue file looks like:

<template>
  <div id="note-editor">
    <textarea
      :value="activeNoteText"
      @input="editNote"
      class="form-control">
    </textarea>
  </div>
</template>

<script>
import { editNote } from '../vuex/actions'

export default {
  vuex: {
    getters: {
      activeNoteText: state => state.activeNote.text
    },
    actions: {
      editNote
    }
  }
}
</script>

Wrapping Up

That sums it up for this tutorial! I hope this practical introduction to Vuex will help you integrate it into your next VueJs project. I'd like to reiterate that Vuex is geared towards medium-to-large-scale SPAs so I don't recommend that you throw it at every app you make because it may result in more complexity than you initially had.

I'd encourage you to download the completed code, if you haven't already, and browse through it at your own pace to see how things fit together.

As always, if you have any questions you can leave a comment below or hit me up on Twitter @coligo_io and I'll do my best to get back to you right away!