Vuex is a state management library that lets us handle and ultimately store data from our UI. In this article, we'll be introducing you to the concepts around Vuex, how to use it, and how to store your data locally with it.
What is Vuex?
You are probably familiar with the concept of state, which is just a fancy way of saying data. We can store state in Vue within the data()
function itself. For example, in the below Vue component, we are storing a toggle state as false, and whenever we click our button in the template section, we set it to true:
<template>
<button id="myButton" @click="runToggle">My Button</button>
</template>
<script>
export default {
data() {
return {
toggleState: false
}
},
methods: {
runToggle: function() {
this.toggleState = true;
}
}
}
</script>
This works great for components with few interactions, but we start to run into problems if we have lots of different components, all depending on the same data, perhaps across multiple pages. For handling that data, we can use Vuex, which manages all of our data centrally, so we can manipulate and access it easily.
Why use Vuex?
The main reason to use Vuex is when your data structure becomes so complicated, that maintaining and sending it between your components becomes burdensome. Vuex provides a single point to store, manipulate, and get your data - simplifying the process massively. For smaller projects, or small independent components, you won't necessarily need to use Vuex!
Getting Started with Vuex
To get started with Vuex, we first need a valid Vue project. If you're brand new to Vue, read my guide on creating your first Vue Project. After that, within your Vue project folder, install vuex by running the following command:
npm i vuex
Now that Vuex has been installed, we can get started on adding it in our project. To start with, we'll make one central Vuex store.
Within our Vue project, we have a file called src/main.js. Let's add our store there. You can update your main.js file to look like the one below:
import { createApp } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'
// I'm also using a router
import router from './router'
const app = createApp(App);
// Create a store for our to do list items
const store = createStore({
state() {
},
getters: {
},
mutations: {
}
});
// We can chain use() functions, so our app is now using a router and our Vuex store
app.use(router).use(store).mount('#app')
Vuex stores are easy to configure, and since we've used use(store)
when initializing our app, it becomes available everywhere in our application immediately. Let's look at what each of the objects within our store do:
-
state()
- this is where we will store our data (also known as state). Any updates or changes to our data will be reflected within this state() function. -
getters
- this does exactly what you think - it lets us get the data from our store. -
mutations
- these are functions we'll use to update our data. We can add methods within this to update and change state data at will.
State and Getters in Vuex
As discussed previously, state() will store our data, and getters are methods which will get data from our state store
Let's look at an example of a store. Below, I have a state store which returns an object called users, which is an array of different. I've put one in here as an example, but you can leave it empty if you like.
const store = createStore({
state () {
return {
users: [
{ id: '123-123-123', name: 'John Doe', email: 'johndoe@fjolt.com' }
]
}
},
getters: {
users (state) {
// state variable contains our state data
return state.users;
}
}
mutations: {
}
});
The data in our state()
is accessible through methods within getters. I've created one getter function, called users. When this is called, we access the user list via the state variable, which contains all data in our state store. As such, when we return state.users, we get all the users in our state store.
Mutating or Changing Data with Vuex
So now we have a store which holds some data, and a way to get that data through the getter function. The final thing we need to do to have a fully fledged store is to create a mutation methods. These are methods which allow us to change data within the state() store.
mutations: {
addUser(state, newUser) {
if(newUser.id !== undefined && typeof newUser.name == 'string' && typeof newUser.email == 'string') {
state.users.push({
id: newUser.id,
name: newUser.name,
email: newUser.email
})
}
}
}
When we create a new method like addUser
, we create two arguments - one is the state, which is a reference to the state store, and the other is data we are pushing with this mutation. The above function lets us push an object like { id: 'some-id', name: 'Jane Doe', email: 'janedoe@fjolt.com' }
through this mutation, and it will push that value to the Vuex store.
Mutations are Synchronous
Please note that all mutations are synchronous. If you want to use an asynchronous event, you have to use actions. So don't try calling an API or returning a promise within a mutation!
A quick word on actions
If you need to return a promise, or use an asynchronous event in your mutation, you can't use mutations. Instead, use actions. Actions are fundamentally the same as mutations, in that they let us alter our state store, but they return a promise and can be asynchronous. Actions can be added to our Vuex store within the actions property:
const store = createStore({
state () {},
getters: {}
mutations: {},
actions: {
waitASecond: function() {
setTimeout(() => {
// Do something here with state()
}, 1000)
}
}
});
Since actions can be asynchronous, we can use them instead with all sorts of asynchronous events like API calls. So remember: mutations for synchronous events, and actions for asynchronous ones.
How to Use Vuex Mutations and Getters
Now that we've defined a getter and a mutation, we need to use them in our app. These functions are accessible through this.$store. Since we've initialized the Vuex store already in main.js, we don't really need to do anything else at this stage.
Let's create a simple component that leverages our store. All it does is adds a new item to the store, and then console logs all items as stringified JSON:
<template>
<div id="new-user">
<input type="text" placeholder="Add a username.." id="username" ref="username">
<input type="text" placeholder="Add an email.." id="email" ref="email">
<input type="submit" id="submit-user" @click="newUser" value="Submit">
</div>
</template>
<script>
// I am using uuid for the ID for each user
import { v4 as uuidv4 } from 'uuid'
export default {
name: "NewUser",
methods: {
newUser: function() {
// We use "commit" to call mutations in Vuex
this.$store.commit('addUser', {
id: uuidv4(),
name: this.$refs.username.value,
email: this.$refs.email.value
})
// We can access getters via this.$store.getters
let allUsers = JSON.stringify(this.$store.getters.users);
console.log('New User Added!')
console.log(`All Users are here: ${allUsers}`);
}
}
}
</script>
We can access pretty much anything in our store via this.$store
. When a user clicks submit in our template, we call our mutation. You might notice we have written:
this.$store.commit('addUser', {});
That's because we don't call mutations directly with Vuex. Instead, we use commit() to call them. Since our mutation from before was called addUser, we can call that mutation using this.$store.commit('addUser', {})
, where the second object is the data we are passing to our muation.
Then we can submit all of our data to our mutation, which subsequently updates our state store in Vuex. Now we can easily add users to our state store, and have it accessible from any component in our Vue application.
Using Actions
Note: We mentioned actions earlier for asynchronous events. You can use actions in the same way as mutations, only you have to call this.$store.dispatch('actonName', {})
, where the first argument is the action you want to call, and the second is the data you are passing to it.
Using Getters
We've also used our getter to console log all users whenever one is added. To access any getter from Vuex, you just have to use this.$store.getters
. All getters will be stored on that object, so this.$store.getters.users references our users() getter from before.
How to Save Vuex Data to Local Storage
Now we've set up our Vuex store, and we can manipulate or change our store as we need to. The (perhaps surprising) thing about Vuex is it is not persistent. That means when you refresh the page, all the data will disappear. One way to solve this, is to save the data to a database. Another, which will ensure the application will work offline too, is to store it locally using localStorage.
As such, we will look at how to save Vuex data to localStorage, so it will persist after refresh. You can save it to a database with an API too, which will allow users to access their data if they are logged in.
The first thing we have to do, is use the subscribe method on our store. Back in main.js
you can add this to the end of your file:
store.subscribe((mutation, state) => {
// The code inside the curly brackets fires any time a mutation occurs.
// When a mutation occurs, we'll stringify our entire state object - which
// contains our todo list. We'll put it in the users localStorage, so that
// their data will persist even if they refresh the page.
localStorage.setItem('store', JSON.stringify(state));
})
subscribe()
in Vuex fire any time a mutation occurs to our store - that means any time data is added, or removed, the subscribe event will fire.
This subscribe event will store all of the state data we currently have in a localStorage item called store - meaning the entire Vuex store will be saved to the user's local computer.
Maintaining localStorage link with Vue Application
Saving it to localStorage is one thing, but it's another to then show it in the application. For that, we need to make a new mutation in our Vuex mutations, which will replace the entire Vuex state() store with our localStorage data, should it exist:
mutations: {
loadStore() {
if(localStorage.getItem('store')) {
try {
this.replaceState(JSON.parse(localStorage.getItem('store')));
}
catch(e) {
console.log('Could not initialize store', e);
}
}
}
// ... other mutations
}
All this function does, is checks if the localStorage item, store, exists, and if it does, we use replaceState() - a function which replaces the entire state store with anything - to replace it with this localStorage data.
Since we want to run this whenever the app loads we need to add it to the beforeCreate() hook of our App.vue file.
<script>
import { useStore } from 'vuex'
export default {
beforeCreate() {
// Get our store
const store = useStore()
// use store.commit to run any mutation. Below we are running the loadStore mutation
store.commit('loadStore');
}
}
</script>
Again, remember, we call mutations using commit(). We have created a variable called store, since it won't be fully setup in the beforeCreate() hook. Using that, we fire off our loadStore mutation, syncing our localStorage and Vuex store.
Using Modules with Vuex
Since our data store above is quite simple, we have no real need to complicate it with modules. Sometimes, though, you'll have separate pieces of data which you don't want to mix. For that kind of thing, we can use modules, which essentially separates our data into different namespaces so we can individually get, mutate, and store them.
Modules follow the same principle as before, the only difference is we can define multiple Vuex stores:
const userStore = {
namespaced: true,
state() {
return {
users: []
}
},
mutations: { // ... }
getters: { // ... }
}
const articleStore = {
namespaced: true,
state() {
return {
articles: []
}
},
mutations: { // ... }
getters: { // ... }
}
const store = createStore({
modules: {
users: userStore,
articles: articleStore
}
})
Now we have two logically different data stores. If we wanted to access userStores
, we'd find it on this.$store
, as that still holds all of our combined stores.
Accessing Module Getters
In the above example, since we are storing our data slightly differently, we need to use this.$store.getters['user/users'] to access our users getter. If we had a getter called usernames, we'd similarly access it by using this.$store.getters['users/usernames']
.
Accessing Module Mutations
Similar to before, we can still access all mutations via this.$store.commit() - only, we need to add our namespace as well. To use a mutation called addUser in the userStore, we'd write this.$store.commit('users/addUser', {})
.
Conclusion
I hope you've enjoyed this guide to getting started with Vuex. We've covered everything you need to load, save, and persist your Vuex storage data. Let's recap what we've looked at here:
- We created a new Vuex store.
- We've learned how to create getter methods to get Vuex data.
- We've learned how to use mutations and call them with commit(), to change Vuex data.
- We've learned how to use modules to separate out different stores of data
- We've touched upon how actions are asynchronous, while mutations are synchronous.
- We've learned how to persist our Vuex data using localStorage.
If you want to see more Vuex in action, read my full guide to creating a to-do list application in Vue. For more Vue content, it can all be found here.