Using Vuex Stores with Vue
Managing the state of a large Vue app (or React app, or any kind of app, really) can be a bit of a bear. This is true particularly if you have a lot of different piece of data to manage - so I certainly recommend breaking your store up into modules, so as to keep better organized in your code. Here’s how I might implement Vuex to manage the state of my Vue app. The way I implemented Vue on Samuel French for managing searches and shopping cart data is very similar to what you will see here.
Getting set up with multiple stores
In this implimentation of Vuex, the goal is to keep things as tidy and organized as possible. Any given app should have its own store (includes: state, getters, actions, mutations), which would then be imported to the single instance of the Vuex store as a module:
// store.js
import Vue from 'vue';
import Vuex from 'vuex';
import store_a from 'path/to/store_a';
import store_b from 'path/to/store_b';
export const store = () => {
return new Vuex.Store({
modules: {
store_a,
store_b
}
});
};
Store Structure
Any given store would contain its own state, getters, actions, and mutations. At the end of the day, these are all plain JS objects, exported from their own files. To ensure consistency across these objects, they will all import variables to describe events that trigger changes. Let’s say our store is called “search”:
// search/index.js
import actions from './actions';
import getters from './getters';
import mutations from './mutations';
const state = {
searchResults: [],
isFetching: false,
hasError: false
}
export default {
state,
actions,
getters,
mutations
}
Rather than defining something like “searchResults” in your component’s local data, we move anything like this that might need to be updated asynchronously, or shared with another component (as we will see later), into the store instead. We will also give names to the types of actions and mutations we will want to use. These will be referenced by our actions, mutations, as well as inside our components:
// /search/types.js
export const GET_SEARCH_RESULTS = 'GET_SEARCH_RESULTS';
export const GET_SEARCH_RESULTS_SUCCESS = 'GET_SEARCH_RESULTS_SUCCESS';
export const GET_SEARCH_RESULTS_ERROR = 'GET_SEARCH_RESULTS_ERROR';
The use of all-caps here is a convention, and helps to visually identify Vuex-specific behaviours. Further, using variables helps us ensure consistency and reduces the potential for errors in our code. And if we need to, we can change the values of the action/mutation types.
Getters are functions run on values in your state, just like computed properties behave in your component. The first prop is this store’s state, which in this case is the search
store. An optional param is the getters object itself, so you can refer to the result of other getters’ values, like in our showResults
getter:
// search/getters.js
export const getters = {
numberResults(state) {
return state.searchResults.length;
},
filteredResults(state) {
return state.searchResults.filter(result => result.isAllowed)
},
showResults(state, getters) {
return state.isFetching && !state.hasError && getters.numberResults > 0
}
}
Inside our component, we would import some functions defined by Vuex to leverage this store as we see in our next example. These functions are used to pull the store into the component for consumption, by using the mapState
helper. Using the spread syntax, mapState
adds the store’s object keys into our computed properties, which can be accessed elsewhere in our component. Notice the namespaced structure of our store object - we must access the specific store we need: state.STORE_NAME.searchResults
.
Also required is any event type needed, like for calling an asynchronous request, in this case GET_SEARCH_RESULTS
, which is added with the mapActions
helper, again with the spread syntax, adding to our local methods:
// searchForm.vue
import { mapActions, mapState, mapGetters } from 'Vuex';
import { GET_SEARCH_RESULTS } from 'path/to/search/types';
...
methods: {
...mapActions([
GET_SEARCH_RESULTS
])
},
computed: {
...mapState({
searchResults: state => state.search.searchResults
}),
...mapGetters([
'numberResults',
'filteredResults'
])
}
We now have access to any actions defined outside, in our store’s actions. Actions are important because they work asynchronously for us. This is particularly useful when we need to interact with an API, using your usual HTTP methods (get, post, etc). We can also choose to handle errors right here, and call the proper mutation based on the API response, rather than sending back the response to the component directly. Since our types are variables, we can access them using the [varName]
syntax:
// search/actions.js
import axios from 'axios';
import * as type from './types';
export const actions = {
[type.GET_SEARCH_RESULTS]: async ({ commit }, payload) => {
commit(type.GET_SEARCH_RESULTS);
try {
const response = await axios.get('/api-endpoint', payload);
commit(type.GET_SEARCH_RESULTS_SUCCESS, response);
} catch(err) {
commit(type.GET_SEARCH_RESULTS_ERROR, err);
}
}
}
The commit
function, destructured from the default arguments of the action, lets us commit our new data to state, by calling a mutation. Our mutations use the same names as we find is the actions object. Our above example calls three separate commits, with different mutations, allowing us to control our UI based on what is happening with the API call as we mentioned above.
In our mutations, we receive whatever payload is being sent from our action (as the second), and we can now update our state:
// search/mutations.js
import * as type from './types';
export const mutations = {
[type.GET_SEARCH_RESULTS](state, payload) {
state.isFetching = true;
},
[type.GET_SEARCH_RESULTS_SUCCESS](state, payload) {
state.searchResults = [...payload.data];
state.hasError = false;
state.isFetching = false;
}
[type.GET_SEARCH_RESULTS_ERROR](state, payload) {
state.hasError = true;
state.isFetching = false;
}
}
Putting it together
Now that we have put together the Vuex store, let’s look at a more complete example inside a Vue component. In our pretend search form, we call a handleSubmit
when the form is posted, which ultimately calls our action. When the API call is resolved, our state gets updated as a result of the commit functions that call our mutations:
// searchForm.vue
<template>
<form @submit="handleSubmit">
<input type="text" :model="searchTerm" />
<div
v-if="hasError">
There was an error with your search
</div>
<button :class="{'show-spinner': isFetching}">Submit</button>
</form>
</template>
<script>
import { mapActions, mapState, mapGetters } from 'Vuex';
import { GET_SEARCH_RESULTS } from 'path/to/search/types';
export default {
name: 'search-form',
data() {
return {
searchTerm: ''
}
},
methods: {
...mapActions([
GET_SEARCH_RESULTS
]),
handleSubmit() {
// perform validation or data formatting before submitting, if needed
// then pass along our search term to the action
this.GET_SEARCH_RESULTS(this.searchTerm);
}
},
computed: {
...mapState({
isFetching: state => state.search.isFetching,
hasError: state => state.search.hasError,
})
}
}
</script>
What we see here is that the data mapped from our store operates in the Vue template just like local data would. When our store’s isFetching
is true, we can show a spinner gif or some other style in place of our button. Once the API call resolves, the mutation sets isFetching
to false
, and our class disappears.
In exactly the same way, if there is an error with the API call, we commit that to our state, and show the error message in the template. If another call is attempted with success, the mutation sets the store’s hasError
to false, and the message is hidden.
The REAL beauty now, is using this same data inside another component. Let’s imagine that the example above only performs the search. What about showing the results? In another component, we can borrow the same state data:
// searchResults.vue
<template>
<div>
<p>Total results: {{ numberResults }}</p>
<ul v-if="showResults">
<li v-for="filteredResults as result">
<a href="result.href">{{ result.text }}</a>
</li>
</ul>
</div>
</template>
<script>
import { mapState, mapGetters } from 'Vuex';
export default {
name: 'search-results',
computed: {
...mapState({
isFetching: state => state.search.isFetching,
hasError: state => state.search.hasError,
}),
...mapGetters([
'numberResults',
'filteredResults',
'showResults'
])
}
}
</script>
In this case, we can also show our results based on our showResults
getter - it checks whether or not there are results to show, if there are errors, and if we’re not still fetching data.
Vuex store recap
Now we can easily share our search
store between components. By moving our local data and API calls from within our component and into the store, any component can access the search
state, getters, and actions. As a result, our component is cleaner, and our code is reusable across components that need access to search
data.