Vuex basics: Tutorial and explanation

January 4, 2016, 1:45 pm

Update Nov 2016: This is written for a very old version of the vuex api and the code from Dec 2015.

However, this article still gives a very in-depth look about why vuex is important, how it works and how it can make your apps better and easier to maintain.

Vuex is an in-development and prototype library by the creator of Vue.js to help you build larger applications in a more maintainable way by following principles similar to those made popular by Facebook's Flux library (and subsequent iterations by community like redux).

Instead of directly jumping into vuex and how to use it, in this post, I'll explain the rationale behind why it is favorable to alternative approaches and how it is helpful to you.

What are we building?

Target

A simple app with a button and a counter. Pressing the button increments the counter. While this is quite easy to accomplish the goal is to understand the underlying concept.

Let's say there are two components in an app:

  1. A button (which is the source of an event)
  2. A counter (which has to reflect updates based on the original event)

The two components aren't aware of each other and can't communicate with each other. This is a very common pattern in even the smallest web applications. In larger applications dozens of components communicate with each other and have to keep each other in check. Don't believe me? Here's some of the interactions for even a basic todo list:

Goals of this article

We will explore four ways to solve the same problem:

  1. Using event broadcasts to communicate between components
  2. Using a shared state object
  3. Using vuex

After reading this article, hopefully you should be able to understand:

  1. A very basic workflow around how to use vuex in your project.
  2. What kind of problems it solves.
  3. Why it may be a better approach than other methods, despite being a bit more verbose and strict.

Building the starting point

We'll solve the same problem in 3 different approaches. But before that, we need to have a common starting point. If you're planning to follow along, I suggest creating a git repo for the tutorial, making a commit after this point, and branching off different approaches.

$ npm install -g vue-cli
$ vue init webpack vuex-tutorial
$ cd vuex-tutorial
$ npm install
$ npm install --save vuex
$ npm run dev

Now you should see the basic vue scaffolding page. Let's create and update a few files to get to where we want to be.

First, we create the IncrementButton component in src/components/IncrementButton.vue:

<template>
  <button @click.prevent="activate">+1</button>
</template>

<script>
export default {
  methods: {
    activate () {
      console.log('+1 Pressed')
    }
  }
}
</script>

<style>
</style>

Next we create the CounterDisplay component to actually display the counter. Let's create a new basic vue component in src/components/CounterDisplay.vue

<template>
  Count is {{ count }}
</template>

<script>
export default {
  data () {
    return {
      count: 0
    }
  }
}
</script>
<style>
</style>

Replace App.vue with this file:

<template>
  <div id="app">
    <h3>Increment:</h3>
    <increment></increment>
    <h3>Counter:</h3>
    <counter></counter>
  </div>
</template>

<script>
import Counter from './components/CounterDisplay.vue'
import Increment from './components/IncrementButton.vue'
export default {
  components: {
    Counter,
    Increment
  }
}
</script>

<style>
</style>

Now, if you run npm run dev again, and open the page in your browser, you should see a button and a counter. Clicking the button shows a message in the console, nothing else. So now that we've got our starting point, let's proceed.

Solution 1: Event Broadcasts

Let's make the modification to the scripts in the components. First, in IncrementButton.vue we use $dispatch to send a message to parent that the button is clicked

export default {
  methods: {
    activate () {
      // Send an event upwards to be picked up by App
      this.$dispatch('button-pressed')
    }
  }
}

In App.vue we listen to the event from the child and re-broadcast a new event to all children to increment.

export default {
  components: {
    Counter,
    Increment
  },
  events: {
    'button-pressed': function () {
      // Send a message to all children
      this.$broadcast('increment')
    }
  }
}

In CounterDisplay.vue we listen to the increemnt event and increase the value in the state.

export default {
  data () {
    return {
      count: 0
    }
  },
  events: {
    increment () {
      this.count ++
    }
  }
}

Some disadvantages of this approach:

Basically there's nothing technically wrong in this. Then again, there's nothing technically wrong in writing your entire app in one file with all the application logic written exclusively in goto's. It's all about maintainability, and here's how this approach is terrible for maintainablility.

  1. For every action, the parent components need to wire and "dispatch" events to the right components.
  2. It's hard to understand where events might come from for a larger application.
  3. There's no clear place for the "business logic". this.count++ is in CounterDisplay but the business logic can be anywhere, which can make it unmaintainable.

Let me give an example of how this approach can lead to a bug:

  1. You hire two interns: Alice and Bob. You tell Alice you want another counter in another component. You tell Bob to write a Reset button.
  2. Alice writes a new component FormattedCounterDisplay which is subscribed to increment, and increments it's own state. Alice is happy and commits and pushes.
  3. Bob writes a new "Reset" component which emits a reset event to App, which re-dispatches it. He implements reset on CounterDisplay to set count to 0 but he is not aware that Alice's component is subscribed to it as well.
  4. Your user presses "+1" and sees the app is working ok. But when the user presses "reset", only one counter resets.

This may seem like a very simple example, but it's just to illustrate how scattering state and business logic and tying it together with events can lead to errors.

Solution 2: Shared state

Let's revert everything we did in Solution 1. We create a new file src/store.js

export default {
  state: {
    counter: 0
  }
}

Let's first modify the CounterDisplay.vue:

<template>
  Count is {{ sharedState.counter }}
</template>

<script>
import store from '../store'

export default {
  data () {
    return {
      sharedState: store.state
    }
  }
}
</script>

We're doing quite a few interesting things here:

  1. We're getting the store object which is just one constant object. But this is defined in a different file.
  2. In our local data we're creating a new parameter called sharedState mapping to store.state
  3. Since it's part of data vue makes store.state reactive, meaning vue will automatically update sharedState whenever anything in store.state changes

So far it still won't work. But now, we can modify IncrementButton.vue

import store from '../store'

export default {
  data () {
    return {
      sharedState: store.state
    }
  },
  methods: {
    activate () {
      this.sharedState.counter += 1
    }
  }
}
  1. In this, we import store and subscribe to the reactive state just like in the previous example.
  2. When the activate function is called, it goes to the sharedState which still refers to store.state and increments the counter
  3. All the components and computed properties subscribed to counter will now be updated.

How this is better than Solution 1

Let's re-visit the problem of the two interns - Alice and Bob.

  1. Alice writes FormattedComponentDisplay to subscribe to the Shared State Counter which will always show the latest counter value
  2. Bob's ResetButton component sets the Shared State Counter to 0. This will afect both CounterDisplay and FormattedCounterDisplay that alice wrote.
  3. User notices that the reset button works as expected

How this is still not good enough.

  1. Over the period of their itnernship, Alice and bob write multiple counter displays, reset buttons, and increment buttons in different formats which all update the same shared counter. Life is good.
  2. Once they go back to college, you have to maintain their code.
  3. Carol - the new manager comes in and says "I never want the counter to go above 100"

What do you do now?

  1. Do you go to all the dozens of components and find out all the places they are updating the counter? That's frustrating.
  2. Do you go to the displays and add a filter/formatter there? That's frustrating too.

And here-in lies the problem. The Business logic is scattered all across the application. This may be a simple issue in principle but it can be a big pain to maintain, and debug.

A slightly better approach

Now that you refactor all the original code you re-write your store.js like this:

var store = {
  state: {
    counter: 0
  },
  increment: function () {
    if (store.state.counter < 100) {
      store.state.counter += 1;
    }
  },
  reset: function () {
    store.state.counter = 0;
  }
}

export default store

This makes the code a lot cleaner since you're explicitly calling increment and all your business logic is in the store. However, a new intern who doesn't know the theory behind all this finds it's easier to simply write to store.state.counter from some other part of the app and it becomes hard to debug.

You then make lots of strict rules, guidelines, code reviews to ensure nobody writes to the state without using a function in store.js and when that doesn't work, you go talk to human resources to shut down the intern program.

Solution 3: Vuex

Let's revert all the changes we did to Solution 2. In principle vuex works along somewhat similar lines of Solution 2. Here's a slightly scary looking diagram:

Let's first create the src/store.js again but this time we have this code:

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

Vue.use(Vuex)

var store = new Vuex.Store({
  state: {
    counter: 0
  },
  mutations: {
    INCREMENT (state) {
      state.counter ++
    }
  }
})

export default store

So let's see what's happening in this code:

  1. We're getting the Vuex module and instructing Vue to use it to activate the plugin.
  2. Our store is no longer a plain json object but an instance of Vuex.Store
  3. We create a counter in the state again, set to 0
  4. We have a new mutations object which has INCREMENT, takes an input state, and modifies that state.

Here are some interesting things in this code:

  1. Everything that does require('../store.js') or import store from '../store.js' will use the same store instance
  2. We never edit store.state.counter but we get a copy of the state which we should update and modify. This will be important later.

Now that we've covered the store, let's go to the IncrementButton.vue

import store from '../store'

export default {
  methods: {
    activate () {
      store.dispatch('INCREMENT')
    }
  }
}

This component doesn't even have any data. But on the click we call store.dispatch('INCREMENT') . We'll come back to this soon.

Now, update CounterDisplay.vue

<template>
  Count is {{ counter }}
</template>

<script>
import store from '../store'

export default {
  computed: {
    counter () {
      return store.state.counter
    }
  }
}
</script>

This is when things get truly interesting. We no longer subscribe to shared state. Instead, we use vue's computed properties to pull in the counter from the store.

Vue is smart enough to figure out that the counter computed property depends on the store.state.counter so whenever the store is updated, it will update all related items. And that's it!

If you reload the page, you'll see that the counter is working fine. Here's what's happening step by step:

  1. Vue's event handler calls activate. This function written by us calls store.dispatch('INCREMENT').
  2. Here, INCREMENT is the name of an action. It represents an identifier of "This is the type of change that should be made to the state". We can pass extra params to the dispatch function as well which contains additional parameters for the action.
  3. Vue figures out what mutator to call for the dispatch. Right now we just have one, but we can make this more complex, and customize this for larger apps.
  4. The mutator recieves a copy of the state and updates it. Vue keeps an older copy of the state which can be used for advanced features later.
  5. When the state is updated, vue automatically updates all components dependent on that aspect of state.
  6. This makes your code more testable, if you're into that sort of thing.

Here's why this is SO MUCH BETTER than solution 2:

  1. If a copy of all states are kept during development, the vue developers can potentially build what's known as a "Time Travelling Debugger". Aside from sounding from one of the coolest super-hero names, it will allow you to 'undo' actions in your app and change the logic and develop a lot faster.
  2. You can build middleware to work whenever states change. For example, you can build a logger which logs all actions performed by a user. If they find a bug, you can get that log, re-play all those actions and reproduce their bug exactly.
  3. By forcing you to have all actions in one place, it becomes a nice reference that anyone in your team can use about all the ways your app's state can be modified.

Still a long way to go.

This is just barely scratching the surface of vuex can do. Which in itself is still a very early build, which I'm sure will mature into one of the most established development paradigms for many years to come.

You can find a lot more information on how to organize your stores, and more info on the vuex documentation. It might take some time to internalize all the concepts and it might even take some trial-and-error to figure out the right balance and approach.

Epilogue: Dealing with the intern's code

So you ported your app to vue.js and your intern still figures out he can write to store.state.counter as a short-cut in his components. And you've had it, that's the last straw. You go ahead and add one line to your store.js

var store = new Vuex.Store({
  state: {
    counter: 0
  },
  mutations: {
    INCREMENT (state) {
      state.counter ++
    }
  },
  strict: true // Vuex's patent pending anti-intern device
})

Now any time anyone directly writes to the store, an error will be thrown. Do note that this slows your app down a bit, and you can safely remove it for production. See the docs for an example how to do it.

Next Post Previous Post