Note: this proposal has been heavily revised based on feedback so some of the comments below may be out of context.
1. Terms naming change for better semantics
Dispatching a mutation never sounded right. Dispatch should be indicating the intention for something to happen. For mutations, we want a verb that indicates the state change is happening as soon as you call it.
- The old
store.dispatch
is now store.commit
.
store.dispatch
will be used for firing actions instead. (see next section)
The new naming better conveys the semantics behind the two methods:
- "dispatching an action": indicating the intention for something to happen (possibly async with side effects).
- "committing a mutation": indicating the synchronous transaction of actual state change.
2. Module Portability & Composability
Actions inside store and modules
There has been common requests on shipping actions with the store, or inside modules. Previously, the reason for not putting actions in the store/modules was mainly how do we access them. Actions defined inside the store means we need access to the store - again the singleton problem, which is now a non-issue.
Now since they are just functions, we may expose them as store.actions
and call them directly. In fact this was the API of Vuex 0.4.x:
store.xxx // reserved for Store class methods/properties
store.actions.xxx // this seems ok
A problem is when actions are defined inside modules, what if multiple modules define actions of the same name? Also, sometimes we may want to call an action that affect multiple stores, just like mutations.
It seems actions are also more like event listeners. Instead of calling them directly as functions, we now use store.dispatch
to trigger them:
const store = new Vuex.Store({
actions: {
doSomething: ({ commit }) => {
commit('some-mutation')
}
}
})
store.dispatch('doSomething')
This way, you can dispatch actions in multiple modules with a single call, just like mutations. It's also more explicit that you are firing off some side-effects in your store, instead of just calling a random function.
Getters, too
You can now define getters in the store / modules too. Similar to module mutations, module getters receive the sub state tree:
const store = new Vuex.Store({
state: {
count: 0
},
getters: {
hasAny: state => state.count > 0
}
})
// access the getter
store.getters.hasAny // -> false
3. Composable Action Flow
Now that we are putting actions inside modules and calling them via dispatch
, we somehow lose out on the composability of actions, because they are no longer just functions you can call. Here's how we can make them composable again:
-
To indicate the completion of an action, return a Promise from the action. store.dispatch
will return that Promise if there is only a single handler called. If multiple action handlers are matched, it will return a Promise that resolves when all Promises returned by those handlers are resolved.
const store = new Vuex.Store({
actions: {
doSomething: ({ commit }, payload) => {
return callPromiseAPI(payload).then(res => {
commit('some-mutation', { res })
})
}
}
})
store.dispatch('doSomething', { id: 1 }).then(() => {
// action done
})
-
Based on (1) and async/await
, we can have very clean composition between async actions:
const store = new Vuex.Store({
actions: {
one: async ({ commit }, payload) => {
const res = await callPromiseAPI(payload)
commit('some-mutation', { res })
},
two: async ({ dispatch, commit }) => {
await dispatch('one')
commit('done')
}
}
})
store.dispatch('two') // fires off complicated async flow
The convention of returning Promises also allows Vuex to:
- better handle errors during async action flow.
- simplify store initialization during server-side rendering.
Component Binding
The design of Vuex 0.6~1.0 had a somewhat annoying design constraint: avoid directly accessing stores inside components. The store is injected at the root component, and implicitly used via the vuex: { getters, actions }
options. This was in preparation for Vue 2.0 SSR (server-side rendering), because in common SSR setups (directly requiring the component in Node.js and render it), dependence on a global singleton will cause that singleton to be shared across multiple requests, thus making it possible for a request to pollute the state of the next one.
However, with the new bundleRenderer
strategy implemented in Vue 2.0.0-alpha.7, this is no longer an issue. The application bundle will be run in a new context for each request, making it unnecessary to structure your app without singletons just for the sake SSR. This also means it's totally fine to just import store from './store'
, use plain computed properties to return store.state.xxx
, or calling store.dispatch()
in plain methods.
This opens up path to simplifying the component binding usage, since theoretically you don't need any binding at all. Currently, the vuex
options feels a bit clumsy and indirect.
In Vuex 2.0, the vuex
option will be deprecated in favor of just computed properties and methods. You are free to structure your Vuex store usage the way you prefer. However, we will be keeping the injection for this.$store
so that you can do this:
export default {
computed: {
a () {
return this.$store.getters.a
}
},
methods: {
b (...args) {
this.$store.dispatch('b', …args)
}
}
}
The above alleviates the need to import the store everywhere. But it can get verbose when you have many getters and actions in the same component. Therefore we provide two helpers, mapGetters
and mapActions
:
import { mapGetters, mapActions } from 'vuex'
export default {
computed: mapGetters(['a', 'b', 'c']),
methods: mapActions(['d', 'e', 'f'])
}
So in the component, this.a
maps to this.$store.getters.a
, and this.d(...args)
maps to this.$store.dispatch('d', ...args)
.
If you want to map a getter/action to a different local name, use an object instead:
import { mapGetters, mapActions } from 'vuex'
export default {
computed: mapGetters({
myComputed: 'a' // map this.myComputed to store.getters.a
}),
methods: mapActions({
myMethod: 'b' // map this.myMethod() to store.dispatch('b')
})
}
Finally, you can easily compose them with local computed properties and methods using Object spread operator:
import { mapGetters, mapActions } from 'vuex'
export default {
computed: {
localComputed () { … },
...mapGetters(['a', 'b', 'c', 'd'])
},
methods: {
localMethod () { … },
...mapActions(['b'])
}
}
2.0 so soon? How about 1.0?
1.0 contains small breaking changes but should be a very easy upgrade for existing 0.6~0.8 users. It will be maintained as a stable release in parallel to 2.0.