visit
STORE
At the center of any Vuex application is a store. The store is a container that stores the state of your application. Two points distinguish Vuex store from a simple global object:The Vuex store is reactive. When Vue components rely on their state, they will be reactively and efficiently updated if the state of the store changes.You cannot directly change the state of the store. The only way to make changes is to cause a mutation explicitly. This ensures that any change in the state leaves a mark and allows the use of tools to better understand the progress of the application.After installing Vuex, a repository is created. It's quite simple, and you need to specify the initial state object and some actions and mutations.const store = new Vuex.Store({
state: {
counter: 0 // initial store state
},
actions: {
increment({ commit, dispatch, getters }) {
commit('INCREMENT')
},
decrement({ commit, dispatch, getters }) {
commit('DECREMENT')
}
},
mutations: {
INCREMENT(state) {
state.counter++
},
DECREMENT(state) {
state.counter--
}
},
getters: {
counter(state) {
return state.counter
}
}
})
The reason we are committing a mutation instead of changing
store.state.count
directly, is because we want to explicitly track it. This simple convention makes your intention more explicit, so that you can reason about state changes in your app better when reading the code. In addition, this gives us the opportunity to implement tools that can log every mutation, take state snapshots, or even perform time travel debugging.STATE. SINGLE STATE TREE
Vuex uses a single state tree when one object contains the entire global state of the application and serves as the only one source. It also means that the app will have only one such storage. A single state tree makes it easy to find the part you need or take snapshots of the current state of the application for debugging purposes.The data you store in Vuex follows the same rules as the
data
in a Vue instance, ie the state object must be plain. So how do we display state inside the store in our Vue components? Since Vuex stores are reactive, the simplest way to "retrieve" state from it is simply returning some store state from within a computed property. Whenever
store.state.count
changes, it will cause the computed property to re-evaluate, and trigger associated DOM updates.This pattern causes the component to rely on the global store singleton. When using a module system, it requires importing the store in every component that uses store state, and also requires mocking when testing the component. Vuex provides a mechanism to "inject" the store into all child components from the root component with the
$store
option (enabled by Vue.use(Vuex)
)export default {
methods: {
incrementCounter() {
this.$store.dispatch('increment')
}
}
}
When a component needs to make use of multiple store state properties or getters, declaring all these computed properties can get repetitive and verbose. To deal with this we can make use of the
mapState
helper which generates computed getter functions for us, saving us some keystrokes:import { mapState } from 'vuex';
export default {
computed: {
...mapState({
counter: state => state.counter
}),
counterSquared() {
return Math.pow(this.counter, 2)
}
}
}
We can also pass a string array to
mapState
when the name of a mapped computed property is the same as a state sub tree name.Note that
mapState
returns an object. How do we use it in combination with other local computed properties? Normally, we'd have to use a utility to merge multiple objects into one so that we can pass the final object to computed
. However with the object spread operator (which is a stage-4 ECMAScript proposal), we can greatly simplify the syntax as shown above.Using Vuex doesn't mean you should put all the state in Vuex. Although putting more state into Vuex makes your state mutations more explicit and debuggable, sometimes it could also make the code more verbose and indirect. If a piece of state strictly belongs to a single component, it could be just fine leaving it as local state. You should weigh the trade-offs and make decisions that fit the development needs of your app.GETTERS
Sometimes we may need to compute derived state based on store state, for example filtering through a list of items and counting them.If more than one component needs to make use of this, we have to either duplicate the function, or extract it into a shared helper and import it in multiple places - both are less than ideal.Vuex allows us to define "getters" in the store. You can think of them as computed properties for stores. Like computed properties, a getter's result is cached based on its dependencies, and will only re-evaluate when some of its dependencies have changed.// In store
getters: {
counter(state) {
return state.counter
},
counterSquared(state) {
return Math.pow(state.counter, 2)
}
}
// In component
import { mapGetters } from 'vuex';
export default {
computed: {
...mapgetters([ 'counter', 'counterSquared' ])
}
}
The
mapGetters
helper simply maps store getters to local computed properties.MUTATIONS
The only way to actually change state in a Vuex store is by committing a mutation. Vuex mutations are very similar to events: each mutation has a string type and a handler. The handler function is where we perform actual state modifications, and it will receive the state as the first argument.You cannot directly call a mutation handler. Think of it more like event registration:"When a mutation with type "increment" is triggered, call this handler."
To invoke a mutation handler, you need to call
store.commit
with its type.export default {
methods: {
incrementCounter() {
this.$store.commit('INCREMENT')
}
}
}
You can pass an additional argument to
store.commit
, which is called the payload for the mutation. In most cases, the payload should be an object so that it can contain multiple fields, and the recorded mutation will also be more descriptive. An alternative way to commit a mutation is by directly using an object that has a type
property. When using object-style commit, the entire object will be passed as the payload to mutation handlers, so the handler remains the same.Since a Vuex store's state is made reactive by Vue, when we mutate the state, Vue components observing the state will update automatically. This also means Vuex mutations are subject to the same reactivity caveats when working with plain Vue:Vue.set(obj, 'newProp', 123)
, or replace that Object with a fresh one. For example, using the object spread syntax.You can commit mutations in components with
this.$store.commit('xxx')
, or use the mapMutations
helper which maps component methods to store.commit calls (requires root $store
injection)Asynchronicity combined with state mutation can make your program very hard to reason about. For example, when you call two methods both with async callbacks that mutate the state, how do you know when they are called and which callback was called first? This is exactly why to separate the two concepts. In Vuex, mutations are synchronous transactions. To handle asynchronous operations, should descry Actions.ACTIONS
Actions are similar to mutations with a few differences:actions: {
signIn({ commit }, payload) {
// Show spinner when user submit form
commit('LOGIN_IN_PROGRESS', true);
// axios - Promise based HTTP client for browser and node.js
axios
.post('/api/v1/sign_in', {
email: payload.email
password: payload.password
})
.then((response) => {
const { user, token } = response.data;
commit('SET_AUTH_TOKEN', token);
commit('SET_USER', user);
commit('LOGIN_IN_PROGRESS', false);
})
.catch((error) => {
commit('SET_SIGN_IN_ERROR', error.response.data.reason);
commit('LOGIN_IN_PROGRESS', false);
})
}
}
Action handlers receive a context object which exposes the same set of methods/properties on the store instance, so you can call
context.commit
to commit a mutation, or access the state and getters via context.state
and context.getters
. We can even call other actions with context.dispatch
. We will see why this context object is not the store instance itself when we introduce Modules later.In practice, we often use ES2015 argument destructuring to simplify the code a bit especially when we need to call
commit
multiple times. Actions are triggered with the store.dispatch
method. This may look silly at first sight if we want to increment the count, why don't we just call store.commit('increment')
directly? Remember that mutations have to be synchronous? Actions don't. We can perform asynchronous operations inside an action. Actions support the same payload format and object-style dispatch.A more practical example of real-world actions would be an action to checkout a shopping cart, which involves calling an async API and committing multiple mutations. Performing a flow of asynchronous operations, and recording the side effects (state mutations) of the action by committing them.You can dispatch actions in components with
this.$store.dispatch('xxx')
, or use the mapActions
helper which maps component methods to store.dispatch
calls (requires root $store
injection). Actions are often asynchronous, so how do we know when an action is done? And more importantly, how can we compose multiple actions together to handle more complex async flows?The first thing to know is that
store.dispatch
can handle Promise returned by the triggered action handler and it also returns Promise. It's possible for a store.dispatch
to trigger multiple action handlers in different modules. In such a case the returned value will be a Promise that resolves when all triggered handlers have been resolved.Also, in our blog, you can read more about Vue.js:
Previously published at