Some breaking changes currently being considered.
Usage in components
Currently, all components that need to display store state or invoke store actions import the store directly. This makes the component definitions rely on a global singleton, and leads to the following drawbacks:
- A bit harder to test: the store needs to be mocked in order to test the component in isolation.
- Makes server side rendering hard, because ideally we want a fresh store instance for each request.
So the first thing we can do, is instead of directly importing the store in child components, we only import it at the root component and "inject" it into all child components:
// Vue.use(Vuex) teaches Vue instances to handle the `store` option
import store from '../store'
import App from '../components/App.vue'
// render root
new Vue({
el: '#app',
store, // provide store, and inject to all children
components: { App }
})
Now, the store will automatically be available to all children as this.$store
(similar to how this.$router
is injected to all components enabled by vue-router). However, this makes things a bit more verbose and repetitive:
// example component after the change
export default {
computed: {
someState () {
return this.$store.state.someState
},
computedState () {
const { valueA, valueB } = this.$store.state
return valueA + valueB
}
},
methods: {
someAction (...args) {
this.$store.actions.someAction(...args)
}
}
}
So, the idea is introducing a vuex
option:
import { someAction } from '../store/actions'
export default {
vuex: {
state: {
someState: state => state.someState,
computedState: ({ valueA, valueB }) => valueA + valueB
},
actions: {
someAction, // becomes available as a method `this.someAction`
// ad-hoc inline action
inlineAction: ({ dispatch }, ...args) => dispatch('SOME_MUTATION', ...args)
}
}
}
Note what's happening here:
vuex.state
is basically sugar for computed properties. The difference is that the getters will get passed the store's state as the argument.
vuex.actions
is sugar for mapping actions to methods. Note **we are no longer exposing the actions on the store itself. Now the store is only concerned with state and mutations. Actions are simply functions that take store as the first argument and dispatch mutations on it. What the vuex.actions
option does, is essentially binding raw action functions to this.$store
, and then defining it as a directly callable method on the component. Here we are using the Object literal shorthand, so the method name will be the same (someAction
). You can of course use a method name different from the imported function.
Now, why do we no longer expose actions on the store? Several reasons:
- Users have been asking about how to "namespace" the actions, because all actions share the same namespace on
store.actions
. By directly importing actions where needed, this problem no longer exists.
- This allows you to see where the action functions are defined right inside the component, thus allowing more flexibility in where to place them (instead of all inside a single
actions.js
file). For example you can split them up into store modules, and don't need to worry about not knowing which module an action is defined in.
Because actions are defined without directly depending on the store (instead takes it as an argument), importing them doesn't cause the singleton problem. When they are bound by vuex.actions
internally, they just use whatever store the component has.
Module composition
Currently, module composition is a bit awkward:
import * as moduleA from './modules/a'
import * as moduleB from './modules/b'
export default new Vuex.Store({
state: {
a: moduleA.state,
b: moduleB.state
},
mutations: [moduleA.mutations, moduleB.mutations]
})
This can get annoying when there are many modules. Also, mutations inside modules always get the whole state tree. This leads to:
- A module's mutation can actually mutate sub trees managed by other modules;
- A module's mutation needs knowledge of how modules are composed in order to "resolve" the sub tree it owns. This makes it hard to develop decoupled store modules.
Here's an idea for a better composition API:
// a module
const state = {
count: 0
}
const mutations = {
INCREMENT: state => state.count++, // receives only the sub tree owned by this module
DECREMENT: state => state.count--
}
export default {
state,
mutations
}
// composing modules
import counter from './modules/counter'
const inlineModule = {
state: { n: 123 },
mutations: { INCREMENT: state => state.n++ }
}
export default new Vuex.Store({
state, // global state...
mutations, // global mutations...
modules: {
counter, // adds and manages `store.state.counter`
inline: inlineModule
}
})
So modules essentially become "sub stores" that manages a sub tree of the state.
Unresolved questions
Actions still get the entire store. Which means if an action is shipped within a module, it has no way to know how to resolve its local state.