When you have a small application, using emit
and props
may be sufficient but as an application grows, it will become tidious to manage all the events.
With a global state, it will be simplier and safer to handle the data.
Plus, the state is more easily debuggable.
A Vuex instance contains:
- a property
state
, equivalent to thedata
property in a component. - a property
getters
, equivalent to thecomputed
property in a component. - a property
actions
, equivalent to themethods
property in a component.
The new thing is the mutations
property. It is responsible to set and update data.
See:
actions
as the methods to retrieve external data and call mutations with the data.mutations
as the methods to update the data contained in the store, and only that.
In addition, always make mutations
as simple as possible, while you could have complex actions.
Any mutation method takes:
- a
state
instance as first parameter - a
payload
data as a second parameter
Any getter method takes:
- a
state
instance as first parameter - a
getters
array as a second parameter. It corresponds to all existing getters.
In a component, you use simply as the following:
import { computed } from 'vue';
import store from '@/store/index';
const products = computed(() => store.getters.availableProducts);
If you ever need to pass to a getter a parameter other than state
or getters
, you will need to return a function.
For example:
- in the the store, you declare such getter:
isProductInStock() {
return (product) => product.inventory > 0;
},
- in the component script, you add a computed property:
//using Composition API
const isProductInStock = computed(() => store.getters.isProductInStock);
- and in the component template, you pass the object or data the getter needs via the computed variable:
<button
@click="addProductToCart(product)"
:disabled="!isProductInStock(product)"
>
Add to cart
</button>
Any action receives a context
parameter that allow to access:
state
commit
You can destructure the context
to use only the commit
.
Well, if you look at the documentation of Vuex 4, you will see that you need to update:
// src/store/index.js
import { createStore } from 'vuex';
export const store = createStore({
state: {
data: []
},
getters: {
//getters go here
},
actions: {
//actions go here
},
mutations: {
//mutations go here
},
});
Then import and use the store in the Vue instance:
import { createApp } from 'vue';
import { store } from '@/store';
import App from './App.vue';
createApp(App).use(store).mount('#app');
Finally, you need to import the useStore
composable from vuex
in the components that need it:
//using the Composition API
import { computed, ref } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
const loading = ref(true);
store.dispatch("fetchData").then(() => loading.value = false);
const data = computed(() => store.getters.availableData);
It is really easy to see the mutation history under DevTools > Vue > Vuex
.
If you have like I had a difficulty to see the mutation, reinstall the Vue.js devtools.
With the tool, you can easily go back and forward in the timeline with a simple click.
The advantage of vuex map helpers is to make the code less verbose:
import { computed } from 'vue';
const products = computed(() => store.state.products);
const isProductInStock = computed(() => store.getters.isProductInStock);
const productInventoryMessage = computed(() => store.getters.productInventoryMessage);
versus
import { mapState, mapGetters } from '@/store/mapState'
const { products } = mapState();
const { isProductInStock, productInventoryMessage } = mapGetters();
The code above is using Composition API and the helper library suggested by Markus Kottländer on this Stackoverflow thread.
With the Option API, you can use the mapState
, mapActions
and mapGetters
from the vuex
package and achieve a similar result.
See this commit from the lesson "Vuex Map Helpers " on the course "Vuex for Everyone".
It comes a time when a store can be big. Using the split pattern, you can extract state, getters, actions and mutations to a distinct file.
To organize a state, you can go further than multiple files using modules.
In the end, the store folder would look this:
src
|_ store
|_ index.js
|_ state.js
|_ getters.js
|_ actions.js
|_ mutations.js
|_ modules
|_ firstModule.js
|_ secondModule.js
You could also extract the state
,getters
, actions
and mutations
to separate files in a firstModule
folder.
An advice: do not use createStore
in the modules. Simply export default { ... }
. That module object contains the same properties as the root state:
export default {
namescaped: true,
state: {
//state properties go here
},
getters {
//getters go here
},
actions: {
// etc...
},
mutations,
strict: true,
}
It is good practice to use namescaped
to avoid name collisions.
Simply, tell the action of module Y that the action is in the rootGetters
object:
methodOfModuleY(
{ state, commit, getters, rootState, rootGetters },
payload,
) {
commit('mutationOfModuleY', true);
if (!rootGetters['moduleX/methodToCall'](payload)) {
// do something...
}
}
Another way is the following: simply, tell the action of module Y that the mutation is in at the root.
methodOfModuleY(
{ state, commit, dispatch },
payload,
) {
//set payload to null if none
dispatch('moduleX/anotherMethod', payload, { root: true });
}
Simply, tell the action of module Y that the mutation is in at the root:
methodOfModuleY(
{ state, commit, dispatch },
payload,
) {
//set payload to null if none
commit('moduleX/aMutationInX', payload, { root: true });
}
You will have the mapState
, mapGetters
, mapActions
and mapMutations
available.
Using the spread operator, you get access to the properties or methods using different syntax:
...mapGetters({
getterX1: "moduleX/getter1"
getterY1: "moduleY/getter1"
})
//or
...mapGetters("moduleX", {
getterX1: "getter1"
getterX2: "getter2"
})
I had more difficulty to figure it out but here how it looks.
Supposing you have a store organized as the following:
src
|_ store
|_ index.js
|_ state.js
|_ getters.js
|_ actions.js
|_ mutations.js
|_ modules
|_ firstModule.js
|_ state
|_ property1
|_ property2
|_ getters
|_ getter1
|_ getter2
|_ actions
|_ action1
|_ action2
|_ mutations
|_ secondModule.js
|_ state
|_ getters
|_ getter1
|_ getter2
|_ actions
|_ action1
|_ action2
|_ mutations
You can use it this way using the mapStore
helper library suggested by Markus Kottländer on this Stackoverflow thread:
import { useStore } from 'vuex';
import { mapGetters } from '@/store/mapStore'
const { ["moduleX/getter1"]: getterX1, ["moduleX/getter2"]: getterX2, ["moduleY/getter1"]: getterY1 } = mapGetters();
const actionY1 = (payload) => {
store.dispatch('moduleY/action1', payload);
}
So in the end, only the mapGetters are really needed in the helper library, in my experience.
Also, if you find yourself wanting to use mapState
from that library, tell me how you make it work when using namescaped modules. I used a getter to access for example moduleX.state.property1
.
I hope you learn a lot on Vuex.
At the time of writing this article, Vuex has been replaced by Pinia and VueSchool have a course on the new recommended state management of Vue.
Stay tuned for the notes of that course!