WTF is Vuex? A Beginner's Guide To Vuex 4
Anthony Gore | May 15th, 2017 (Updated October 12th, 2020) | 11 min read
Vuex is an essential tool in the Vue.js ecosystem. But developers new to Vuex may be repelled by jargon terms like "state management pattern" and confused as to what they actually need it for.
Here's the primer on Vuex that I wish I'd had when I started learning. In it, I'll cover the high-level concepts of Vuex and show you how to use Vuex in an app.
2020/10/05: this tutorial has now been updated for Vue 3 & Vuex 4!
Table of contents:
Vuex
Vuex. Is it pronounced "vewks", or "veweks"? Or maybe it's meant to be "vew", pronounced with a French-style silent "x"?
My trouble with understanding Vuex only began with the name.
Being an eager Vue developer I'd heard enough about Vuex to suspect that it must be an important part of the Vue ecosystem, even if I didn't know what it actually was.
I eventually had enough of wondering, so I went to the documentation with plans of a brief skim through; just enough to get the idea.
To my chagrin, I was greeted with unfamiliar terms like "state management pattern", "global singleton" and "source of truth". These terms may make sense to anyone already familiar with the concept, but for me, they didn't gel at all.
The one thing I did get, though, was that Vuex had something to do with Flux and Redux. I didn't know what those were either, but I figured it may help if I investigated them first.
After a bit of research and persistence, the concepts behind the jargon finally started to materialize in my mind. I was getting it. I went back to the Vuex documentation and it finally hit me...Vuex is freaking awesome!
I'm still not quite sure how to pronounce it, but Vuex has become an essential piece in my Vue.js toolbelt. I think it's totally worth your time to check it out too, so I've written this primer on Vuex to give you the background that I wish I'd had.
The problem that Vuex solves
To understand Vuex it's much easier if you first understand the problem that it's designed to solve.
Imagine you've developed a multi-user chat app. The interface has a user list, private chat windows, an inbox with chat history, and a notification bar to inform users of unread messages from other users they aren't currently viewing.
Millions of users are chatting to millions of other users through your app daily. However, there are complaints about an annoying problem: the notification bar will occasionally give false notifications. A user will be notified of a new unread message, but when they check to see what it is it's just a message they've already seen.
What I've described is a real scenario that the Facebook developers had with their chat system a few years back. The process of solving this inspired their developers to create an application architecture they named "Flux". Flux forms the basis of Vuex, Redux, and other similar libraries.
Flux
Facebook developers struggled with the "zombie notification" bug for some time. They eventually realized that its persistent nature was more than a simple bug - it pointed to some underlying flaw in the architecture of the app.
The flaw is most easily understood in the abstract: when you have multiple components in an application that share data, the complexity of their interconnections will increase to a point where the state of the data is no longer predictable or understandable. Consequentially the app becomes impossible to extend or maintain.
The idea of Flux was to create a set of guiding principles that describe a scalable frontend architecture that sufficiently mitigates this flaw. Not just for a chat app, but in any complex UI app with components and shared data state.
Flux is a pattern, not a library. You can't go to Github and download Flux. It's a design pattern like MVC. Libraries like Vuex and Redux implement the Flux pattern the same way that other frameworks implement the MVC pattern.
In fact, Vuex doesn't implement all of Flux, just a subset. Don't worry about that just now though, let's instead focus on understanding the key principles that it does observe.
Principle #1: single source of truth
Components may have local data that only they need to know about. For example, the position of the scroll bar in the user list component is probably of no interest to other components.
But any data that is to be shared between components, i.e. application data, needs to be kept in a single place, separate from the components that use it.
This single location is called the "store". Components must read application data from this location and not keep their own copy to prevent conflict or disagreement.
import { createStore } from "vuex";
// Instantiate our Vuex store
const store = createStore({
// "State" is the application data your components
// will subscribe to
state () {
return {
myValue: 0
};
}
});
// Components access state from their computed properties
const MyComponent = {
template: "<div>{{ myValue }}</div>",
computed: {
myValue () {
return store.state.myValue;
}
}
};
Principle #2: data is read-only
Components can freely read data from the store. But they cannot change data in the store, at least not directly.
Instead, they must inform the store of their intent to change the data and the store will be responsible for making those changes via a set of defined functions called "mutations".
Why this approach? If we centralize the data-altering logic than we don't have to look far if there are inconsistencies in the state. We're minimizing the possibility that some random component (possibly in a third-party module) has changed the data in an unexpected fashion.
const store = createStore({
state() {
return {
myValue: 0
};
},
mutations: {
increment (state, value) {
state.myValue += value;
}
}
});
// Need to update a value?
// Wrong! Don't directly change a store value.
store.myValue += 10;
// Right! Call the appropriate mutation.
store.commit('increment', 10);
Principle #3: mutations are synchronous
It's much easier to debug data inconsistencies in an app that implements the above two principles in its architecture. You could log commits and observe how the state changes in response (which you can indeed do when using Vuex with Vue Devtools).
But this ability would be undermined if our mutations were applied asynchronously. We'd know the order our commits came in, but we would not know the order in which our components committed them.
Synchronous mutations ensure state is not dependent on the sequence and timing of unpredictable events.
Cool, so what exactly is Vuex?
With all that background out of the way we're finally able to address this question - Vuex is a library that helps you implement the Flux architecture in your Vue app. By enforcing the principles described above, Vuex keeps your application data in a transparent and predictable state even when that data is being shared across multiple components.
Now that you have a high-level understanding of Vuex, let's see how we'd actually create a Vuex-based application.
Setting up a Vuex to-do app
To demonstrate usage of Vuex we're going to set up a simple to-do app. You can access a working example of the code here.
If you'd like to develop this on your local machine, the quickest way to get up and running is by creating a Vue CLI application, so let's do that:
$ vue create vuex-example
Be sure to include Vue 3 in the Vue CLI options, but don't include Vuex - we want to add that ourselves so we can learn how to install it!
Installing Vuex
Once the Vue CLI installation is complete, change into the project directory. Now we'll install Vuex, and run the server.
$ cd vuex-example
$ npm i -S vuex@4
$ npm run serve
At the time of writing Vuex 4 is still in beta. To use it, you'll have to install the beta version with the command
npm i -S vuex@4.0.0-beta.4
.
Creating a Vuex store
Now we are ready to create our Vuex store. To do this, we'll create a JavaScript module file at src/store/index.js.
$ mkdir src/store
$ touch src/store/index.js
Let's now open the file and import the createStore
method. This method is used to define the store and its features, which we'll do in a moment. For now, we'll export the store so it can easily be added to our Vue app.
src/store/index.js
import { createStore } from "vuex";
export default createStore({});
Adding the store to a Vue instance
To ensure you can access your Vuex store from within any component, we'll need to import the store module in the main file, and install the store as a plugin on the main Vue instance:
src/main.js
import { createApp } from "vue";
import App from "@/App";
import store from "@/store";
const app = createApp(App);
app.use(store);
app.mount("#app");
Creating a simple app
The point of Vuex, as discussed, is to create scalable, global state, usually in large applications. We can demonstrate its features in a simple to-do app, however.
Here's what this app will look like when it's complete:
Let's now delete the boilerplate component file added to the Vue CLI installation:
$ rm src/components/HelloWorld.vue
TodoNew.vue
We'll add a new component now, TodoNew
, which will have the job of creating new to-do items.
$ touch src/components/TodoNew.vue
Open that file and let's begin with the template. Here we'll define a form with a text input allowing the user to enter a to-do task. This input is bound to a data property task
.
src/components/TodoNew.vue
<template>
<form @submit.prevent="addTodo">
<input
type="text"
placeholder="Enter a new task"
v-model="task"
/>
</form>
</template>
Moving to the component definition now, there are two local state properties - task
, described above, and id
which gives a new to-do item a unique identifier.
Let's stub a method addTodo
which will create the todo item. We'll be accomplishing this with Vuex shortly.
src/components/TodoNew.vue
<template>...</template>
<script>
export default {
data() {
return {
task: "",
id: 0
};
},
methods: {
addTodo: function() {
//
}
}
};
</script>
Defining store state
In a moment, we're going to create a component that displays our to-do items. Since both it and the TodoNew
component need to access the same data, this will be the perfect candidate for global state which we'll hold in our Vuex store.
So let's return to our store now and define the property state
. We'll assign a function to this which returns an object. This object has one property, todos
which is an empty array.
src/store/index.js
import { createStore } from "vuex";
export default createStore({
state () {
return {
todos: []
}
}
});
Note: the store state is a factory function to ensure the state is fresh every time the store is invoked.
Defining mutations
As we know from principle #2, Vuex state cannot be directly mutated - you need to define mutator functions.
So let's add a mutations
property to the store, now, and add a function property addTodo
. All mutators receive the store state as their first argument. The second optional argument is the data that components calling the mutator can pass in. In this case, it will be a to-do item.
In the function body, let's use the unshift
method to add the new to-do item to the top of the todo array list.
src/store/index.js
import { createStore } from "vuex";
export default createStore({
state () {
return {
todos: []
}
},
mutations: {
addTodo (state, item) {
state.todos.unshift(item);
}
}
});
Using mutations i.e. "commits"
Okay, now we have enough of our store setup that we can use it in the TodoNew
component. So, let's go back to the component and complete the addTodo
method we stubbed.
First, let's destructure the Vue context object to get copies of the id
and task
local data values.
To access the store we can use the global property this.$store
. We'll now use the commit
method to create a new mutation. This gets passed two arguments - firstly, the name of the mutation, and secondly, the object we want to pass, which will be a new to-do item (consisting of the id
and task
values).
After this, don't forget we'll need to iterate the id by going this.id++
and clear our input value by putting this.task = ""
.
src/components/TodoNew.vue
methods: {
addTodo: function() {
const { id, task } = this;
this.$store.commit("addTodo", { id, task });
this.id++;
this.task = "";
}
}
Review
Let's review this component again to ensure you have a clear picture of how it works:
- The user enters their todo item into the input, which is bound to the
task
data property - When the form is submitted the
addTodo
method is called - A to-do object is created and "committed" to the store.
src/components/TodoNew.vue
<template>
<form @submit.prevent="addTodo">
<input
type="text"
placeholder="Enter a new task"
v-model="task"
/>
</form>
</template>
<script>
export default {
data() {
return {
task: "",
id: 0
};
},
methods: {
addTodo: function() {
const { id, task } = this;
this.$store.commit("addTodo", { id, task });
this.id++;
this.task = "";
}
}
};
</script>
Reading store data
Now we've created functionality in both UI and app state for adding to-do items. Next, we're going to display them!
TodoList.vue
Let's create a new component for this, TodoList
.
$ touch src/components/TodoList.vue
Here's the content of the template. We'll use a v-for
to iterate through an array of to-do items, todos
.
src/components/TodoList.vue
<template>
<ul>
<li
v-for="todo in todos"
:key="todo.id"
>
{{ todo.description }}
</li>
</ul>
</template>
todos
will be a computed property where we'll return the contents of our Vuex store. Let's stub it for now and complete it in a moment.
src/components/TodoList.vue
<script>
export default {
computed: {
todos() {
//
}
}
};
</script>
Defining getters
Rather than accessing the store content directly, getters are functions that are like computed properties for the store. These are perfect for filtering or transforming data before returning it to the app.
For example, below we have getTodos
which returns the state unfiltered. In many scenarios, you may transform this content with a filter
or map
.
todoCount
returns the length of the todo array.
Getters help in fulfilling principle #1, single-source of truth, by ensuring components are tempted to keep local copies of data.
src/store/index.js
export default createStore({
...
getters: {
getTodos (state) {
return state.todos;
},
todoCount (state) {
return state.todos.length;
}
}
})
Back in our TodoList
component, let's complete the functionality by returning this.$store.getters.getTodos
.
src/components/TodoList.vue
<script>
export default {
computed: {
todos() {
return this.$store.getters.getTodos;
}
}
};
</script>
App.vue
To complete this app, all there is to do now is import and declare our components in App.vue.
src/App.vue
<template>
<div>
<h1>To-Do List</h1>
<div>
<TodoNew />
<TodoList />
</div>
</div>
</template>
<script>
import TodoNew from "@/components/TodoNew.vue";
import TodoList from "@/components/TodoList.vue";
export default {
components: {
TodoNew,
TodoList
}
};
</script>
That's it! We now have a working Vuex store.
Do you actually need Vuex?
It's clear that in a large application, having a global state management solution is going to help keep your app predictable and maintainable.
But in this simple to-do app, you'd be justified in thinking Vuex is overkill. There's no clear point where Vuex is necessary or unnecessary, but if you're aware of the pros and cons you can probably intuit this yourself.
Pros of Vuex:
- Easy management of global state
- Powerful debugging of global state
Cons of Vuex:
- An additional project dependency
- Verbose boilerplate
As it was said by Dan Abramov, "Flux libraries are like glasses: you’ll know when you need them."
One possible alternative in Vue 3 is to roll your own Vuex using the Composition API. It doesn't give you the debugging capabilities of Vuex, but it's a lightweight alternative that may work in small projects.
You can read more about this in my article Should You Use Composition API as a Replacement for Vuex?
About Anthony Gore
If you enjoyed this article, show your support by buying me a coffee. You might also enjoy taking one of my online courses!
Click to load comments...