State management in Vue.js
understanding-vuex-the-easy-guide-to-state-management-in-vue-js
Video
State management
Why do we need state management
When dealing with large applications, we definitely have to pass information between components. Shopping card needs to know whether the user is logged in or not, the search needs to get updated based on changes to filter data, and many more scenarios.
The usual ways of passing data such as props only gets us just that far. Things get complex when we want to pass the data in multiple layers of component hierarchy. Or even when we want to pass data into components outside of those hierarchies.
At the end of the day, it's important to have a single source of truth and that's what Vue is offering to solve using vuex. But consider using a state management library if:
- Multiple views may depend on the same piece of state.
- Actions from different views may need to mutate the same piece of state
Vuex
vuex is an Elm-inspired state management library. It integrates really well into vue-devtools, providing zero-setup access to time travel debugging.
Vuex is based on Flux (a state management design pattern introduced by Facebook). The TLDR version looks like:
Installation
If you haven't used Vue CLI to create your project, there are three steps you need to take:
Install the library:
npm install vuex --save
Import and wire it up:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
And last, because Vuex requires Promise you should include a package like es6-promise and import it before using Vuex:
npm install es6-promise --save
And:
import 'es6-promise/auto'
💡 This library will automatically use a polyfill if the browser doesn't support ES6's Promise.
Getting started
In its simplest form, you can use a centralised store as such:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++;
}
}
})
Now, you can access the state object as store.state, and trigger a state change with the store.commit function:
console.log(store.state.count) // -> 0
store.commit('increment')
console.log(store.state.count) // -> 1
Vuex state
This library uses a single state tree which ensures that the source of truth is always the state. However, this doesn't have any conflicts with the modularity. We will have a look at the sub-modules later.
Vuex's state consist of four objects, state, mutations, actions, and getters. So lets see what each of these are. But first, let's see how we can use the store in our components. We use our previous students example from here on.
Including store in your components
Vuex provides a mechanism to inject the store into all child components from the root component with the store option which happens when you use Vue.use(Vuex):
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
students: [
{
name: 'Yas',
age: 25
}
]
}
})
const app = new Vue({
el: '#app',
store
})
This will allow you to access state using this.$store in any child component.
const Students = {
template: `
<ul>
<li v-for="student in students">{{ student.name }}</li>
</ul>`,
computed: {
students () {
return this.$store.state.students
}
}
}
Note how I accessed the student in a computed property. This will ensure that we rerender the page whenever store changes.
How to use multiple properties from state
If you have to use multiple properties from store, Vuex offers the mapState helper method. Using this method you have the ability to map multiple properties from state to your computed properties:
import { mapState } from 'vuex';
export default {
name: "Students",
computed: mapState({
students: state => state.students,
teachers: state => state.teachers
})
};
As you can see, you can use an arrow function to retrieve the state and access it's properties. You can also define aliases to keep your code cleaner and avoid repeating state.property everywhere you want to use it.
If the local and store property names matches, you can skip the arrow function and just pass an array of property names you want to have:
import { mapState } from 'vuex';
export default {
name: "Students",
computed: mapState([
'students',
'teachers'
])
};
If you have some local computed properties, you could use the spear operator to mix them with mapState:
import { mapState } from 'vuex'
export default {
// ...
computed: {
myLocalProp() {
return {
name: 'Yas'
}
},
...mapState({
numOfStudents: state => state.students.length,
students: state => state.students
})
}
}
Getters
Sometimes you need to have derived properties based on store's state. For example if you want to know how many students are older than 25. You can implement this in your component like so:
<template>
<div>Total number of students aged 25 or older is {{numOfStudents}}</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
numOfStudents: state => state.students
.filter(s => s.age > 24).length;
})
}
</script>
But this means that if another component wants to have this logic, they need to duplicate the code. A better approach to these sorts of derived state is to use getters.
const store = new Vuex.Store({
state: {
students: [
{ id: 1, name: 'Yas', age: 26 }
]
},
getters: {
numOf25OrOlderStudents: state => {
return state.students
.filter(s => s.age > 24).length;
}
}
})
These sort of properties appear under store.getters:
computed: {
numOf25OrOlderStudents () {
return this.$store.getters.numOf25OrOlderStudents
}
}
Similar to mapState, you have mapGetter so you can get these properties easier:
computed: {
...mapGetters([
'numOf25OrOlderStudents',
])
}
Mutations
A mutation is the only way you can change the state in Vuex. These mutations are basically events which have two properties:
- A string type
- A handler You'll perform the modifications inside the handler and it will receive the state as it's first argument:
const store = new Vuex.Store({
state: {
students: [
{ id: 1, name: 'Yas', age: 26 }
]
},
mutations: {
addStudent (state, student) {
// mutate state
state.students.push(student)
}
}
})
💡 You can't call a mutation handler directly, instead, you will need to call store.commit method.
const student = { id: 2, name: 'Alex', age: 23 }
store.commit('addStudent', student)
Like React Redux's actions, it's best to use constants for these mutation types:
// mutation-types.js
export const ADD_STUDENT_MUTATION = 'ADD_STUDENT'
// store.js
import Vuex from 'vuex'
import { ADD_STUDENT_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
[ADD_STUDENT_MUTATION] (state, student) {
state.students.push(student)
}
}
})
The mutations should be synchronous. This is an important rule which you should adhere to so that Vuex can capture before and after mutation states. We will see how to have async state mutations using actions.
Actions
Actions are very similar to mutations, but there are two differences between the two. The first one is that instead of mutating the state themselves, they will commit mutations. And the second one is that actions can have asynchronous operations.
const store = new Vuex.Store({
state: {
students: [
{ id: 1, name: 'Yas', age: 26 }
]
},
mutations: {
addStudent (state, student) {
// mutate state
state.students.push(student)
}
},
actions: {
addStudent (context, id) {
const student = await apiService.get(id)
context.commit('addStudent', student)
}
}
})
The first thing to note here is that actions won't receive the store object, they will get a context object which exposes the same properties. We will see why when we review the modules. And the second one is that there is an asynchronous operation happening here.
You can use ES6 Object Destructuring to get only commit passed down to the action. This is useful if you have multiple commits in your actions because it will keep the code way less.
actions: {
async addStudent ({ commit }, id) {
const student = await apiService.get(id)
commit('addStudent', student)
}
}
You can trigger an actions using store.dispatch method.
store.dispatch('addStudents', 2)
Again, similar to mapState you have mapActions to be able to dispatch an action in your components instead of using this.$store.dispatch method which is way much cleaner. The difference is, you will call this in your method objects:
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'addStudent', // map `this.addStudent()` to `this.$store.dispatch('addStudent')`
// `mapActions` also supports payloads:
'addStudent' // map `this.addStudent(id)` to `this.$store.dispatch('addStudent', id)`
]),
...mapActions({
add: 'addStudent' // map `this.add()` to `this.$store.dispatch('addStudent')`
})
}
}
Modules
As we've seen so far, the state can easily get super big as the project grows. This can lead to multiple issues, but we can use modules to break this single state into multiple modules which have their own state, getters, mutations, actions, and guess what, sub modules 😍.
const studentStore = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const teachersStore = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: studentStore,
b: teachersStore
}
})
store.state.studentStore // -> `studentStore`'s state
store.state.teachersStore // -> `teachersStore`'s state
Inside the state and getters, the module's state is passed down.
const studentStore = {
state: { students: [] },
mutations: {
addStudent (state, student) {
// `state` is the local module state
state.students.push(student)
}
},
getters: {
count (state) {
return state.students.length
}
}
}
However, inside the actions, you can access the global store from context via context.rootStore:
const moduleA = {
// ...
actions: {
async addStudentIfRegistrationOpen ({ commit, rootState }, id) {
if (rootState.registrationOpen) {
const student = await apiService.get(id)
commit('addStudent', student)
}
}
}
}
💡 By default the getters, mutations, and actions are registered on the global namespace. This will allow multiple modules to react to same mutation or action if they want to. You can use the namespaced: true flag on the module to make it self contained.
Top comments (0)