An alternative approach to structuring a vuex store
Antonio Villagra De La Cruz
Posted on August 24, 2021
When using vuex
to manage the state of a large-enough Vue project, it can sometimes be difficult to manage, even more so when using modules. Actions being dispatched are namespaced strings. Accessing the state in the store can sometimes be messy (getters are sometimes frown upon). Also, should the business logic be in an "action" or a "mutation" (or, even, in a "getter")?
To try to add a sensible approach to managing vuex
stores, here is a proposal:
Modules, no name spaces
First, let's quickly look at the folder structure. The /store
will be composed of a /modules
folder, which will then host different subsets of state.
Each module will then have its folder (for example, store/modules/user
), inside which there will be different files: actions.js
, getters.js
, mutations.js
, state.js
, types.js
(more on that later), and finally index.js
to wrap everything together.
The main difference with a more common setup is that we won't be using name spaces, as this would break the focal point of this approach: types.
Only getters, single mutation
Before looking into types though, another convention of this approach is to only use getters
to access the state of the store. This might sound overkill if all the getters do is return a field of the state, but this approach brings consistency in accessing the store and will really shine with, you've guessed it, types!
For the sake of simplicity as well, we will only define a single mutation for each module as follow:
mutations.js
const mutations = {
update(state, { key, value }) {
state[key] = value;
},
};
export default mutations;
All types
It is probably personal preference, but I particularly dislike having hand-written strings all over the code. For one, typos are very easily made, and static analysis tools (such as ESLint) can't really help you. You also need to remember how a particular action, or getter, is named, and that can become difficult to track when working on a large codebase and being part of a team.
For that reason, this whole approach is based on using constant variables instead of strings. Similarly to what I have seen in the redux
world, we will be defining types for actions, getters, and keys (more on mutations later).
In practice, that means defining types like follows:
types.js
export const USER_GETTER_CURRENT = "g/user/current";
export const USER_GETTER_FEED = "g/user/feed";
export const USER_GETTER_OVERVIEW = "g/user/overview";
export const USER_ACTION_GET_CURRENT = "a/user/getCurrent";
export const USER_ACTION_GET_FEED = "a/user/getFeed";
export const USER_ACTION_GET_OVERVIEW = "a/user/getOverview";
export const USER_KEY_CURRENT = "k/user/current";
export const USER_KEY_FEED = "k/user/feed";
export const USER_KEY_OVERVIEW = "k/user/overview";
export const USER_KEY_DETAILS = "k/user/details";
Which will then be used in the other files of the module as such:
actions.js
import api from "@/api";
import {
USER_ACTION_GET_CURRENT,
USER_ACTION_GET_FEED,
USER_ACTION_GET_OVERVIEW,
USER_KEY_CURRENT,
USER_KEY_FEED,
USER_KEY_OVERVIEW,
} from "@/store/types";
const actions = {
[USER_ACTION_GET_CURRENT]({ commit }) {
return api.get(`/user`).then((res) => {
commit("update", { key: USER_KEY_CURRENT, value: res.data });
});
},
[USER_ACTION_GET_FEED]({ commit }) {
return api.get(`/feed`).then((res) => {
commit("update", { key: USER_KEY_FEED, value: res.data });
});
},
[USER_ACTION_GET_OVERVIEW]({ commit }) {
return api.get(`/overview`).then((res) => {
commit("update", { key: USER_KEY_OVERVIEW, value: res.data });
});
},
};
export default actions;
getters.js
import {
USER_GETTER_CURRENT,
USER_GETTER_FEED,
USER_GETTER_OVERVIEW,
USER_KEY_CURRENT,
USER_KEY_FEED,
USER_KEY_OVERVIEW,
} from "@/store/types";
const getters = {
[USER_GETTER_CURRENT](state) {
return state[USER_KEY_CURRENT];
},
[USER_GETTER_FEED](state) {
return state[USER_KEY_FEED];
},
[USER_GETTER_OVERVIEW](state) {
return state[USER_KEY_OVERVIEW];
},
};
export default getters;
state.js
import {
USER_KEY_CURRENT,
USER_KEY_FEED,
USER_KEY_OVERVIEW,
USER_KEY_DETAILS,
} from "@/store/types";
const state = () => ({
[USER_KEY_CURRENT]: {},
[USER_KEY_FEED]: [],
[USER_KEY_OVERVIEW]: [],
[USER_KEY_DETAILS]: {},
});
export default state;
This might seem like a lot of extra verbosity for an arguably minor problem, but stick with me, as this approach really shines when interacting with the store from components!
Blissful components
Finally, all this hard work leads us to the pay off!
To summarize, we have built our vuex
store with the following guidelines:
- modules, no name spaces
- only getters, single mutation
- all types
Now let's see how we might use this in components, and the main benefits of this approach:
App.vue
<template>
...
</template>
<script>
import { computed, ref } from "vue";
import { useStore } from "vuex";
import {
USER_ACTION_GET_CURRENT,
USER_GETTER_CURRENT,
} from "@/store/types";
...
export default {
components: {
...
},
setup() {
const store = useStore();
store.dispatch({ type: USER_ACTION_GET_CURRENT });
...
const user = computed(() => store.getters[USER_GETTER_CURRENT]);
...
return {
...
};
},
};
</script>
Here we already see all the benefits of this approach:
- we get strong guarantees that we won't write a type, if using static analysis tools like ESLint (we even get auto-complete in some IDEs).
- we can see at a glance what actions a component might dispatch, and, because we can only access state through getters, we can also see at a glance which data is being accessed
So, there you have it. There are a bit more blows and whistles to gel all these bits together, but this is the gist of it.
Please feel free to also share any feedback you might have from your own experience using vuex
to manage the state of a Vue
application.
Posted on August 24, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.