Any Vue app that is more than a few components big is going to have shared state.
Without any state management system, we can only share state between parent and child components.
This is useful, but it's limited. We need to be able to share state regardless of the relationship of the components.
To do this in a Vue app, we can use Vuex. It’s the official state management library for Vue apps.
In this article, we'll look at how to share state with Vuex.
Getting Started
To get Vuex into our Vue app, we have to include the Vuex library. Then, we create a Vuex store with the initial state, mutations, and getters.
For example, we can create a simple store for our Vuex to store the count
state and use it in our app as follows:
index.js
:
const store = new Vuex.Store({
state: {
result: {}
},
mutations: {
fetch(state, payload) {
state.result = payload;
}
},
getters: {
result: state => state.result
}
});
new Vue({
el: "#app",
store,
methods: {
async fetch() {
const res = await fetch("https://api.agify.io/?name=michael");
const result = await res.json();
this.$store.commit("fetch", result);
}
},
computed: {
...Vuex.mapGetters(["result"])
}
});
index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<title>App</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex"></script>
</head>
<body>
<div id="app">
<button @click="fetch">Fetch</button>
<p>{{result.name}}</p>
</div>
<script src="./index.js"></script>
</body>
</html>
In the code above, we added the script
tags for Vue and Vuex in index.html
.
Then, we create a div with ID app
to house our Vue app. Inside the div, we add the button to call a method to increment the count
state.
Then, we show the count
in the p
tag.
In index.js
, we write the code for the Vuex store by writing:
const store = new Vuex.Store({
state: {
result: {}
},
mutations: {
fetch(state, payload) {
state.result = payload;
}
},
getters: {
result: state => state.result
}
});
The code above gives us a simple store that stores the result
state initially set to an empty object.
Then, we added our fetch
mutation in the mutations
section, which takes the state
object that has the Vuex state. We then get the data from the payload
and parameter and set the state.result
to payload
each time the fetch
action is dispatched.
Also, we added a getter for the result
, which is a function that takes the state
of the store and then returns the count
from it.
After we create the store, we use it in our Vue app by adding the store
property into the object we passed into the Vue
constructor.
Additionally, we get the result
state in our Vue app by using the mapGetters
method, which maps our getter to the result
computed property in our Vue app.
Then, we wrote a fetch
method, which references the $store
instance and calls commit
with the mutation name passed in.
We passed in the ’fetch’
string to commit the increment
mutation.
Finally, our button has the @click
prop and it's set to the fetch
method.
In the end, when we click on the Fetch button, we should see ‘michael’ displayed from the store’s data as the API data is fetched.
We can also pass in the action type and payload as an object into the commit
method as follows:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state, payload) {
state.count += payload.amount;
}
},
getters: {
count: state => state.count
}
});
new Vue({
el: "#app",
store,
methods: {
increment() {
this.$store.commit({
type: "increment",
amount: 2
});
}
},
computed: {
...Vuex.mapGetters(["count"])
}
});
In the code above, we changed mutations
to:
mutations: {
increment(state, payload) {
state.count += payload.amount;
}
},
Then the increment
method is changed to:
increment() {
this.$store.commit({
type: "increment",
amount: 2
});
}
Mutations can only run synchronous code. If we want to run something asynchronously, we have to use actions.
Vuex can't keep track of mutations that are asynchronous since asynchronous code doesn’t run sequentially.
We can also use the mapMutations
method like the mapGetters
method for getters to map mutations.
To use the mapMutations
method, we can write the following code:
index.js
:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state, payload) {
state.count += payload;
}
},
getters: {
count: state => state.count
}
});
new Vue({
el: "#app",
store,
methods: {
...Vuex.mapMutations({
increment: "increment"
})
},
computed: {
...Vuex.mapGetters(["count"])
}
});
In the code above, we mapped the increment
mutation to the increment
method in our Vue app with the following code:
...Vuex.mapMutations({
increment: "increment"
})
It takes an argument for the payload
as we can see in the @click
listener of the Increment button.
Actions
Actions commit one or more mutations in any way we like.
We can add a simple action to our store and use it as follows:
index.js
:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state, payload) {
state.count += payload;
}
},
getters: {
count: state => state.count
},
actions: {
increment({ commit }, payload) {
commit("increment", payload);
}
}
});
new Vue({
el: "#app",
store,
methods: {
...Vuex.mapActions(["increment"])
},
computed: {
...Vuex.mapGetters(["count"])
}
});
In the code above, we defined the increment
action in the Vuex store as follows:
actions: {
increment({ commit }, payload) {
commit("increment", payload);
}
}
Then we called mapActions
in our Vue app as follows:
methods: {
...Vuex.mapActions(["increment"])
},
The increment
method, which is now mapped to the increment
action, still takes the payload
that we pass in, so we can pass the payload
to the commit
function call in our increment
action, which is then passed into the increment
mutation.
We can also write an asynchronous action by returning a promise as follows:
index.js
:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state, payload) {
state.count += payload;
}
},
getters: {
count: state => state.count
},
actions: {
increment({ commit }, payload) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit("increment", payload);
resolve();
}, 1000);
});
}
}
});
new Vue({
el: "#app",
store,
methods: {
...Vuex.mapActions(["increment"])
},
computed: {
...Vuex.mapGetters(["count"])
}
});
In the code above, we have:
actions: {
increment({ commit }, payload) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit("increment", payload);
resolve();
}, 1000);
});
}
}
which returns a promise that commits the increment
mutation after a second.
Composing Actions
We can compose multiple actions in one action. For example, we can create an action that dispatches multiple actions as follows:
index.js
const store = new Vuex.Store({
state: {
dog: {},
breeds: { message: {} }
},
mutations: {
setDog(state, payload) {
state.dog = payload;
},
setBreeds(state, payload) {
state.breeds = payload;
}
},
getters: {
dog: state => state.dog,
breeds: state => state.breeds
},
actions: {
async getBreeds({ commit }) {
const response = await fetch("https://dog.ceo/api/breeds/list/all");
const breeds = await response.json();
commit("setBreeds", breeds);
},
async getDog({ commit }) {
const response = await fetch("https://dog.ceo/api/breeds/image/random");
const dog = await response.json();
commit("setDog", dog);
},
async getBreedsAndDog({ dispatch }) {
await dispatch("getBreeds");
await dispatch("getDog");
}
}
});
new Vue({
el: "#app",
store,
methods: {
...Vuex.mapActions(["getBreedsAndDog"])
},
computed: {
...Vuex.mapGetters(["breeds", "dog"])
}
});
index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<title>App</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex"></script>
</head>
<body>
<div id="app">
<button @click="getBreedsAndDog">Get Breeds and Dog</button>
<p>{{Object.keys(breeds.message).slice(0, 3).join(',')}}</p>
<img :src="dog.message" />
</div>
<script src="./index.js"></script>
</body>
</html>
In the code above, we have the getBreeds
and getDog
, which are actions with one mutation commit each to the store.
Then we have getBreedsAndDog
which is an action that dispatches the above two actions.
In index.html
, we display all the states that are stored in the store.
We should retrieve the first 3 breed names and also the dog image that we got from the getBreed
and getDog
actions that were called by the getBreedsAndDog
action.
The getBreedsAndDog
action is mapped to the getBreedsAndDog
method, so we can just call it to dispatch the action.
Modules
We can divide our store into modules to segregate actions, mutations, and getters.
For example, we can write two modules and use them as follows:
index.js
:
const dogModule = {
namespaced: true,
state: {
dog: {}
},
mutations: {
setDog(state, payload) {
state.dog = payload;
}
},
getters: {
dog: state => state.dog
},
actions: {
async getDog({ commit }) {
const response = await fetch("https://dog.ceo/api/breeds/image/random");
const dog = await response.json();
commit("setDog", dog);
}
}
};
const countModule = {
namespaced: true,
state: {
count: 0
},
mutations: {
increment(state) {
state.count++;
}
},
getters: {
count: state => state.count
}
};
const store = new Vuex.Store({
modules: {
dogModule,
countModule
}
});
new Vue({
el: "#app",
store,
methods: {
...Vuex.mapActions("dogModule", ["getDog"]),
...Vuex.mapMutations("countModule", ["increment"])
},
computed: {
...Vuex.mapState({
count: state => state.countModule.count,
dog: state => state.dogModule.dog
})
}
});
index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<title>App</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex"></script>
</head>
<body>
<div id="app">
<button @click="getDog">Get Dog</button>
<button @click="increment">Increment</button>
<p>{{count}}</p>
<img :src="dog.message" />
</div>
<script src="./index.js"></script>
</body>
</html>
In the code above, we defined the dogModule
and countModule
, which have the state, mutations, getters, and actions as we have before.
Then, to create our store, we write:
const store = new Vuex.Store({
modules: {
dogModule,
countModule
}
});
Then, when we map our actions and mutations, we have to specify the module as follows:
methods: {
...Vuex.mapActions("dogModule", ["getDog"]),
...Vuex.mapMutations("countModule", ["increment"])
},
Then, when we map getters, we have to write functions to get them from the right module as follows:
computed: {
...Vuex.mapState({
count: state => state.countModule.count,
dog: state => state.dogModule.dog
})
}
Conclusion
We can store shared state in Vue apps with Vuex.
It has state to store the states, getters to get data from states, mutations to change data, and actions to run one or more mutations or other actions and also run them asynchronously.
We can map states into computed properties with mapGetters
and also map mutations and actions with mapMutations
and mapActions
methods respectively.
Finally, we can divide our store into modules to segregate them into smaller pieces.
Before deploying your commercial or enterprise Vue apps to production, make sure you are protecting their code against reverse-engineering, abuse, and tampering by following this tutorial.