Beginning Vuex 4 with Vue 3
Vuex is one of the most useful plugins in the entire VueJS ecosystem. I honestly include Vuex in every app whether it’s an SPA or Monolith. Using Vuex modules allows me to divide up large, dynamic and complex pages or components into maintainable, reusable, scoped components without having to pass a million props
.
I’ve written a few times about Vuex modules on Server Side Up, with Using Vuex Modules Inside Components and Build a Vuex Module. Both I believe are good examples of what you can do with Vuex, I think it only scratches the surface of what Vuex is capable of. The scope of this tutorial is to introduce Vuex in a generic fashion and explain how you can fit it into your project.
Let’s dive in!
What Is Vuex?
According to the official docs on https://vuex.vuejs.org, “Vuex is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.”
I like to define and look at Vuex as a single source of truth that allows you to easily share data between components and pages without having to pass every piece of data through a component’s props. If you’ve ever run into a situation where you have a component that has a million props, tons of defaults, and you can’t seem to keep it in sync, Vuex is the right option.
Before we go too far, with the introduction of the composition API, there’s been some discussion on whether or not Vuex is still relevant or not. This is because the composition API makes code reusability even easer and allows you to share pieces of code through multiple components. I believe the composition API will be extremely helpful in replacing mixins and other functions, but I still love the predictable fashion and pattern that Vuex provides.
When to Use a Vuex Store?
There are a wide variety of times when it makes sense to use a Vuex data store! I’ve touched on a few of them already, but this is how I choose.
When props
becomes unmaintainable
There isn’t a quantitative number of props a component must have before switching to Vuex, but there’s definitely a time where the component becomes difficult to maintain. If you’ve worked with components in the past you’ve probably experienced a point where there should “be a better way” to handle the data. That’s when you need to use Vuex.
Building Massively Complex Forms
This is one area where I found Vuex to be extremely helpful. I used to think it was solely for sharing state across an SPA (which it is also very good at). If you are building a form that changes a state based off of user inputs, my guess is props will start getting hairy between your components. Time to check out Vuex!
Another turning point for me was I wanted to divide my form into multiple, smaller components. These weren’t necessarily re-usable, they were form specific, but my form was well over 2000 lines of code which is un-maintainable. By abstracting my form’s state into separate Vuex modules, I was easily able to divide up the form into multiple components and make it much easier to maintain! We will run through an example on how to do this.
Sharing Data Across an Entire SPA
This is the standard example of when to use Vuex and we run through this a lot in our book. Sharing data across an entire single page application is kind of the go-to for Vuex. Typically global app data would be like a user, notifications, etc. Any piece of data that needs to be present on an entire app, a state management system like Vuex makes this a breeze!
Creating a Page With Multiple Moving Parts
Sometimes you have a page that is super complex. When designing the transaction import page for Financial Freedom, I used a Vuex store. This allowed me to easily share data with other VueJS components. Complicated pages are essentially tiny single page applications. I’ve done this a couple other times in other apps as well where I make a store for a page.
Installing Vuex 4
To install Vuex 4, run the following command:
npm install vuex@next --save
At the time of this writing, Vuex 4 was on the next
branch to give time to update to Vuex 3. Keep an eye out for when the branches are merged.
Setting up Vuex 4 in an Vue 3
I assume you have a working Vue 3 instance and some sort of root file that gets compiled like app.js
. Whatever that file is, open it up and add the following line first:
import { createStore } from 'vuex'
This will allow us to build our store for our app. Next, we need to initialize our data store by adding the following code:
const store = createStore({
modules: {
}
});
We will talk about creating modules next!
Finally, let your Vue 3 app know to use the store by appending .use(store)
to the end of your app definition. This can look like:
createApp({ }).use(store).mount(el)
or like in the Vuex docs:
const app = createApp({});
app.use(store);
Perfect! We now have Vuex ready to go in our Vue app!
Before we dive into Vuex modules, I want to quickly note that you don’t HAVE to use Vuex modules and can just make a few pieces of state to map in your components. However, I’ve honestly never used Vuex this way. I’ve always had enough state to make modules. Even if it’s a few pieces of state, modules really clean up the code base and allow it to grow seamlessly.
Vuex Modules
As I mentioned, everything I do with Vuex is divided into modules. These modules consist of actions
, mutations
, state
and getters
. I touched on these with examples a little bit in “Building a Vuex Module“, but wanted to give a more generic approach.
Let’s break down each one of these pieces from my perspective. The Vuex docs also explains these well.
State
This is the heart of what Vuex helps with. These are the variables/pieces of data that you’d most likely be passing down as props if you didn’t have Vuex. It’s also the data that is shared between pages. Pieces of state are modified via mutations
and retrieved via getters
.
Actions
In a Vuex module, an action is a standard function usually used to prep data before committing to state through a mutation. I’ve placed API requests in actions that load data and commit based on success/failure. I’ve also calculated data in an action before committing to state such as incrementing or keeping track of a value.
Mutations
Mutations are simple methods that simply follow a pattern to modify a piece of state. Say you want to set an active song in a playlist. You’d “commit” a mutation that sets the active song in the playlist. This way each component or method that interacts with your state does so in the proper fashion. Actions also tend to “commit” mutations when they are finished.
Getters
A getter is how you receive a piece of data from state. They are used very heavily in components to make a reference to data.
Creating a Vuex Module
Let’s keep track of a few songs in a playlist. To do this, I first start by creating a store
directory wherever your src
javascript files are. In that directory, I add a module named playlist.js
:
export const playlist = {
state: () => ({
songs: [],
activeIndex: 1,
activeSong: {},
status: 'paused'
}),
actions: {
nextSong( { commit, state } ){
let nextIndex = state.activeIndex + 1;
commit( 'setActiveIndex', nextIndex );
commit( 'setActiveSong', state.songs[ nextIndex ] );
}
},
mutations: {
setActiveIndex( index ){
state.activeIndex = index;
},
setActiveSong( song ){
state.activeSong = song;
}
},
getters: {
getActiveIndex( state ){
return state.activeIndex;
},
getActiveSong( state ){
return state.activeSong;
}
}
}
So there’s a lot to break down but let’s go through it. First of all, I didn’t create all of the getters and mutations for the state simply because we are using this as an example, and it’s all the same process.
Let’s start by looking at our state
. In our state
we initialize a few variables. These will be tracked in our components and will be reactive.
Next up, we have our actions
. If you take a look at the action, we are calling a standard process of nextSong()
. Vuex injects the context
variable which contains all of the available functions and state into the method. I decoupled what we need which is the commit
method and the state
in the method signature. This standardized action prepares what we need before committing to state. In this action, we find the next index number, then commit the mutation to activeIndex
and we find the song at the new index and commit the mutation to set the activeSong
. It’s pretty slick to have a standardized way of performing state modifications.
After actions
we have mutations
. The mutations are simple. They simply accept what is going to be set to a piece of state and set it. However, they are very important since all state needs a process to be modified correctly.
Finally, we have getters
. Getters allow us to retrieve values from our state store. These are used heavily in our components to gain access to the state that we have available.
Now that we have a quick overview, let’s get to the fun implementation!
How to Use Vuex
So we have our Vuex store registered, our first piece of state, let’s get this implemented! The first step we need to take is register our module with our store.
To do this, find where we created the Vuex store:
const store = createStore({
modules: {
}
})
Modify the code to look like:
import { playlist } from './store/playlist.js';
const store = createStore({
modules: {
playlist
}
})
What we are doing is importing our playlist module and registering it with our code! Now we can perform our actions, mutations, and retrieve the values we need!
Sharing State Between Components
Let’s say we want to display the activeSong
from our state in 2 different components in our app. To do that, let’s look at an example component that provides access to our Vuex store:
export default {
computed: {
activeSong(){
return this.$store.getters.getActiveSong();
}
}
}
The first thing to note is that any time you need to reference state in a component, it’s in the computed
property. You can name this property whatever you want (though I think it’d be hard to keep track if the names get crazy), it’s inside of the property is what matters.
Inside of the property, we return a reference to this.$store.getters.getActiveSong()
. Right away you can see the this.$store
reference. We registered the $store
globally within our VueJS app so now we have it available everywhere! Next, we call the getters.getActiveSong()
which returns the value of the getActiveSong()
getter that we created.
That’s all we need to do! We can reference these values through their getters in any component we choose in our app! You may start to see that this could get a little hairy if you have a ton of state. I’ll discuss how to handle that with namespacing
in another tutorial.
Dispatching an Action
The final thing we should touch on is how to dispatch an action to set our state. Let’s say we have a button that fires the next()
method in our component. Implement the method like this:
export default {
methods: {
next(){
this.$store.dispatch('nextSong');
}
}
}
What this method does is simply dispatch
an action with the string name of the action. In our case it’s the nextSong
action. Now our Vuex state will go to the next song in our songs
array!
You can also pass data to your action for pre-processing as well. The second parameter in the action is the data being passed. Say you want to either provide an index to skip to, or go incrementally. You could have an action that looks like:
actions: {
nextSong( { commit, state }, index ){
let nextIndex = null;
if( index ){
nextIndex = index;
}else{
nextIndex = state.activeIndex + 1;
}
commit( 'setActiveIndex', nextIndex );
commit( 'setActiveSong', state.songs[ nextIndex ] );
}
},
Now you can incorporate some passed data into the process before you commit.
Nested Vuex State
One final thing I’d like to touch on in this tutorial is that you can nest Vuex state. I first saw this in an Axios example with InertiaJS where they’d build a form
object and submit just the form
to the API. I thought it was brilliant! It really cleans up the axios requests and scopes the data nicely. With that being said, let’s say we had a form to request a song. The data of the form spans multiple components and we store the form in the Vuex state:
export const playlist = {
state: () => ({
form: {
name: '',
artist: '',
album: ''
}
}),
actions: {
setName( { commit }, data ){
commit( 'setName', data );
}
},
mutations: {
setName( state, name ){
state.form.name = name;
}
},
getters: {
getForm( state ){
return state.form
}
}
}
You can actually provide nested objects in the state! So when it comes time to submit the form to the API all you have to do is submit: this.form
with form
being the name of the computed property that references the state:
export default {
computed: {
form(){
return this.$store.getters.getForm();
}
},
methods: {
requestSong(){
axios.post( '/api/v1/requests', this.form );
}
}
}
I find this syntax really nice for scoping a form so you can easily add fields if needed without having to adjust the axios
request.
Hopefully that helps a little bit and gives a better understanding of the power of Vuex. In the next tutorial, I’ll go into some advanced Vuex methods such as mapping state, actions, using Vuex state as a v-model, namespacing and nested namespacing. Until then, let me know if you have any questions!