State Management with Vuex
In this chapter, we'll be looking at State Management Patterns withVuex.Vuexmay not be needed for every application created, but it is extremely important that you have an understanding of what it is when it becomes appropriate to use it, and how to implement it.
By the end of this chapter, you will have done the following:
- Understood what Vuex is and why you should use it
- Created your first Vuex store
- Investigated actions, mutations, getters, and modules
- Used the Vue devtools to step through Vuex mutations as they happen
What is Vuex?
State management is an important part of modern-day web applications, and managing this state as the application grows is a problem every project faces.Vuexlooks to help us achieve better state management by enforcing a centralized store, essentially a single source of truth within our application. It follows design principles similar to that of Flux and Redux and also integrates with the official Vue devtools for a great development experience.
So far, I've spoken aboutstate_and_managing state, but you may still be confused as to what this really means for your application. Let's define these terms in a little more depth.
State Management Pattern (SMP)
We can define a state as the current value(s) of a variable/object within our component or application. If we think about our functions as simpleINPUT -> OUTPUTmachines, the values stored outside of these functions make up the current condition (state) of our application.
Note how I've made a distinction betweencomponent levelandapplication levelstate. The component level state can be defined as state confined to one component (that is, the data function within our component). Application level state is similar but is often used across multiple components or services.
As our application continues to grow, passing state across multiple components gets more difficult. We saw earlier in the book that we can use an Event bus (that is, a global Vue instance) to pass data around, and while this works, it's much better to define our state as part of a singular centralized store. This allows us to reason about the data in our application much easier, as we can start definingactionsandmutationsthat always generate a new version of state, and managing state become much more systemized.
Event bus is a simple approach to state management relying on a singular view instance and may be beneficial in small Vuex projects, but in the majority of cases, Vuex should be used. As our application becomes larger, clearly defining our actions and intended side effects with Vuex allows us to better manage and scale the project.
A great example of how all this fits together can be seen in the following screenshot(https://vuex.vuejs.org/en/intro.html):
Vuex state flow
Let's break down this example into a step-by-step process:
- Initial State is rendered inside of a Vue component.
- A Vue component dispatches an Action to get some data from a Backend API .
- This then fires a Commit event that is handled by a Mutation . This Mutation returns a new version of the state containing the data from the Backend API .
- The process can then be seen in the Vue Devtools , and you have the ability to "time travel" between different versions of the previous state that takes place within the application.
- The new State is then rendered inside of the Vue Components .
The main component of our Vuex application(s) is, therefore, the store, our single source of truth across all component(s). The store can be read but not directly altered; it must have mutation functions to carry out any changes. Although this pattern may seem strange at first, if you've never used a state container before, this design allows us to add new features to our application in a consistent manner.
As Vuex is natively designed to work with Vue, the store is reactive by default. This means any changes that happen from within the store can be seen in real time without the need for any hacks.
Thinking about state
As a thought exercise, let's start off by defining the goals for our application as well as any state, actions, and potential mutations. You don't have to add the following code to your application just yet, so feel free to read on, and we'll bring it all together at the end.
Let's start off by considering the state as a collection of key/value pairs:
const state = {
count: 0 // number
}
For our counter application, we just need one element of state—the current count. This will likely have a default value of0and will be of type number. As this is likely the only state inside of our application, you can consider this state to be application level at this point.
Next, let's think about any action types that the user may want to take our counter application.
These three action types can then be dispatched to the store and thus we can perform the following mutations, returning a new version of state each time:
- Increment : Add one to the current count (0 - > 1)
- Decrement : Remove one from the current count (1 - > 0)
- Reset : Set the current count back to zero (n - > 0)
We can imagine that at this point, our user interface will be updated with the correct bound version of our count. Let's implement this and make it a reality.
Using Vuex
Now that we've had a detailed look at what makes up an application driven byVuex, let's make a playground project to take advantage of these features!
Run the following in your Terminal:
# Create a new Vue project
$ vue init webpack-simple vuex-counter
# Navigate to directory
$ cd vuex-counter
# Install dependencies
$ npm install
# Install Vuex
$ npm install vuex
# Run application
$ npm run dev
Creating a new store
Let's start off by creating a file namedindex.jsinsidesrc/store. This is the file we'll use to create our new store and bring together the various components.
We can start by importing bothVueandVuexas well as telling Vue that we'd like to use theVuexplugin:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
We can then export a newVuex.Storewith a state object that contains all of our application states. We're exporting this so that we can import the state in other components when necessary:
export default new Vuex.Store({
state: {
count: 0,
},
});
Defining action types
We can then create a file insidesrc/storenamedmutation-types.js, which contains the various actions that the user may take within our application:
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';
Although we don't have to explicitly define our actions like this, it's a good idea to use constants where possible. This allows us to take better advantage of tooling and linting techniques, as well as allowing us to infer the actions within our entire application at a glance.
Actions
We can use these action types to commit a new action to be subsequently handled by our mutations. Create a file insidesrc/storenamedactions.js:
import * as types from './mutation-types';
export default {
[types.INCREMENT]({ commit }) {
commit(types.INCREMENT);
},
[types.DECREMENT]({ commit }) {
commit(types.DECREMENT);
},
[types.RESET]({ commit }) {
commit(types.RESET);
},
};
Inside each method, we're destructuring the returnedstoreobject to only take thecommitfunction. If we didn't do this, we'd have to call thecommitfunction like this:
export default {
[types.INCREMENT](store) {
store.commit(types.INCREMENT);
}
}
If we revisit our state diagram, we can see that after committing an action, the action is picked up by the mutator.
Mutations
A mutation is the only method in which the state of the store can be changed; this is done by committing/dispatching an action, as seen earlier. Let's create a new file insidesrc/storenamedmutations.jsand add the following:
import * as types from './mutation-types';
export default {
[types.INCREMENT](state) {
state.count++;
},
[types.DECREMENT](state) {
state.count--;
},
[types.RESET](state) {
state.count = 0;
},
};
You'll note that once again, we're using our action types to define the method names; this is possible with a new feature from ES2015+ named computed property names. Now, any time that an action is committed/dispatched, the mutator will know how to handle this and return a new state.
Getters
We can now commit actions and have these actions return a new version of the state. The next step is to create getters so that we can return sliced parts of our state across our application.Let's create a new file insidesrc/storenamedgetters.jsand add the following:
export default {
count(state) {
return state.count;
},
};
As we have a minuscule example, the use of a getter for this property isn't entirely necessary, but as we scale our application(s), we'll need to use getters to filter state. Think of these as computed properties for values in the state, so if we wanted to return a modified version of this property for the view-layer, we could as follows:
export default {
count(state) {
return state.count > 3 ? 'Above three!' : state.count;
},
};
Combining elements
In order to pull this all together, we have to revisit ourstore/index.jsfile and add the appropriatestate,actions,getters, andmutations:
import Vue from 'vue';
import Vuex from 'vuex';
import actions from './actions';
import getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
count: 0,
},
actions,
getters,
mutations,
});
In ourApp.vue, we can create atemplatethat will give us the current count as well as some buttons toincrement,decrement, andresetstate:
<template>
<div>
<h1>{{count}}</h1>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">R</button>
</div>
</template>
Whenever the user clicks on a button, an action is dispatched from within one of the following methods:
import * as types from './store/mutation-types';
export default {
methods: {
increment() {
this.$store.dispatch(types.INCREMENT);
},
decrement() {
this.$store.dispatch(types.DECREMENT);
},
reset() {
this.$store.dispatch(types.RESET);
},
},
}
Once again, we're using constants to make for a better development experience. Next, in order to take advantage of the getter we created earlier, let's define acomputedproperty:
export default {
// Omitted
computed: {
count() {
return this.$store.getters.count;
},
},
}
We then have an application that displays the current count and can be incremented, decremented, or reset:
Payloads
What if we wanted to let the user decide how much they wanted to increment the count? Let's say we had a textbox that we could add a number and increment the count by that much. If the textbox was set to0or was empty, we'd increment the count by1.
Our template would, therefore, look like this:
<template>
<div>
<h1>{{count}}</h1>
<input type="text" v-model="amount">
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">R</button>
</div>
</template>
We'd place the amount value on our local component state, as this doesn't necessarily need to be part of the main Vuex Store. This is an important realization to make, as it means we can still have local data/computed values if necessary. We can also update our methods to pass the amount through to our actions/mutations:
export default {
data() {
return {
amount: 0,
};
},
methods: {
increment() {
this.$store.dispatch(types.INCREMENT, this.getAmount);
},
decrement() {
this.$store.dispatch(types.DECREMENT, this.getAmount);
},
reset() {
this.$store.dispatch(types.RESET);
},
},
computed: {
count() {
return this.$store.getters.count;
},
getAmount() {
return Number(this.amount) || 1;
},
},
};
We then have to updateactions.jsas this now receives both thestateobject and ouramountas an argument. When we usecommit, let's also pass theamountthrough to the mutation:
import * as types from './mutation-types';
export default {
[types.INCREMENT]({ commit }, amount) {
commit(types.INCREMENT, amount);
},
[types.DECREMENT]({ commit }, amount) {
commit(types.DECREMENT, amount);
},
[types.RESET]({ commit }) {
commit(types.RESET);
},
};
Therefore, our mutations look similar to before, but this time we increment/decrement based on the amount:
export default {
[types.INCREMENT](state, amount) {
state.count += amount;
},
[types.DECREMENT](state, amount) {
state.count -= amount;
},
[types.RESET](state) {
state.count = 0;
},
};
Ta-da! We can now increment the count based on a text value:
Vuex and Vue devtools
Now that we have a consistent way of interacting with our store via actions, we can take advantage of the Vue devtools to see our state over time. If you haven't installed the Vue devtools already, visitChapter 2,Proper Creation of Vue Projects, to find more information regarding this.
We'll be using the counter application as an example, to ensure that you have this project running, and right click onInspect Elementfrom within Chrome (or your browser's equivalent). If we head over to theVuetab and selectVuex, we can see that the counter has been loaded with the initial application state:
From the preceding screenshot, you can see the count state member as well as the value of any getters. Let's click on theincrementbutton a few times and see what happens:
Awesome! We can see theINCREMENTaction as well as a subsequent change to thestateandgetters, and more information about themutationitself. Let's see how we can time travel throughout our state:
In the preceding screenshot, I've selected thetime travelbutton on the first action. You can then see that our state is reverted tocount: 1, and this is reflected in the rest of the metadata. The application is then updated to reflect this change in state, so we can literally step through each action and see the results on screen. Not only does this help with debugging, but any new state that we add to our application will follow the same process and be visible in this manner.
Let's hit thecommitbutton on an action:
As you can see, this merges all of our actions up to when we hitcommitto then be part of ourBase State. As a result, thecountproperty is then equal to the action you committed toBase State.
Modules and scalability
At the moment, we have everything in root state. As our application gets larger, it would be a good idea to take advantage of modules so that we can appropriately split our container into different chunks. Let's turn our counter state into its own module by creating a new folder insidestorenamedmodules/count.
We can then move theactions.js,getters.js,mutations.js,andmutation-types.jsfiles into the count module folder. After doing so, we can create anindex.jsfile inside the folder that exports thestate,actions,getters,andmutationsfor this module only:
import actions from './actions';
import getters from './getters';
import mutations from './mutations';
export const countStore = {
state: {
count: 0,
},
actions,
getters,
mutations,
};
export * from './mutation-types';
I've also elected to export the mutation types from theindex.jsfile, so we can use these within our components on a per-module basis by importing fromstore/modules/countonly. As we're importing more than one thing within this file, I gave the store the name ofcountStore. Let's define the new module insidestore/index.js:
import Vue from 'vue';
import Vuex from 'vuex';
import { countStore } from './modules/count';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
countStore,
},
});
OurApp.vuethen changes slightly; instead of referencing the types object, we reference the types specifically from this module:
import * as fromCount from './store/modules/count';
export default {
data() {
return {
amount: 0,
};
},
methods: {
increment() {
this.$store.dispatch(fromCount.INCREMENT, this.getAmount);
},
decrement() {
this.$store.dispatch(fromCount.DECREMENT, this.getAmount);
},
reset() {
this.$store.dispatch(fromCount.RESET);
},
},
computed: {
count() {
return this.$store.getters.count;
},
getAmount() {
return Number(this.amount) || 1;
},
},
};
We can then add more modules to our application by having the same files/structure as our count example. This allows us to scale as our application continues to grow.
Summary
In this chapter, we took advantage of theVuexlibrary for consistent state management within Vue. We defined what state is as well as component state and application-level state. We learned how to appropriately split our actions, getters, mutations, and store between different files for scalability as well as how to call these items within our components.
We also looked at using the Vue devtools withVuexto step through mutations as they happened within our application. This gives us the ability to better debug/reason about the decisions we make when developing applications.
In the next chapter, we'll look at testing our Vue applications and how to let our tests drive our component design.