Verbosity in component source code, repetitive copy/pasting when adding new elements to state, getters, actions, and mutations in the store
I'm not sure if this is just specific to my use case, but I'm using a simple store (no modules) to put all shared data in one place and use it across components. I've noticed that every component must then:
mapState, mapGetters, mapActions and mapMutationscomputedI was hoping that just calling ...mapGetters() (et al) with no parameter would add _all_ the defined getters, as a shorthand for listing every one of them, and so on for the other helpers. Is there a reason for the helper not to help in this situation?
Just as an aside, in case it leads to an idea... I had experimented with the rather hideous code below:
created () {
console.log('Getters:', Reflect.ownKeys(this.$store.getters))
}
and that does indeed produce an array of all the getters names. Same works for mutations, with the even more worrying:
console.log('Mutations:', Reflect.ownKeys(this.$store._mutations))
(more because __mutations_). But sadly this can't be used in the computed () section because it seems this.$store isn't initialised by then.
So perhaps that answers my question: mapGetters() can't map all the getters, because it doesn't know what they are yet. If that's the case, I guess this idea is dead...
You can use provide/inject to make those things implicit but it's intentional to have explicit declarations. You can also use a mixin to reuse mapping but I really think it's a bad idea to have a _map everything_ method because we don't want people to use it everywhere for laziness.
You can also use the $store.state.something, $store.dispatch, etc inside of any component without doing anything
I'm sure you're right in general, but in my specific case where pretty much every component needs pretty much every state, getter and mutation, it was becoming a real pain to add in another element to an array in every component every time I added a new mutation. Based on your advice, I did this in store/index.js:
store.allState = Reflect.ownKeys(state)
store.allGetters = Reflect.ownKeys(getters)
store.allMutations = Reflect.ownKeys(mutations)
and then this in main.js
Vue.mixin({
methods: {
...mapMutations(store.allMutations)
},
computed: {
...mapState(store.allState),
...mapGetters(store.allGetters)
}
})
If you don't think it's _too_ bad, perhaps you'd consider adding mapAllState(), etc.? But if not, this recipe works, and I can just duplicate it for other projects.
I agree with @posva
If all things are implicitly injected into components, it would make your app less maintainable and to weaken by changes.
For example, your app will be broken if you add a mutation that has a name conflicts with any component method.
I know sometimes the approach that injecting all things is convenient but it is usually in case of very small apps. As Vuex is designed for large applications, it would not be in Vuex.
That's fine - this is a relatively small app, and the benefits of speed of development outweigh the risk for me in this case.
For example, your app will be broken if you add a mutation that has a name conflicts with any component method.
I don't see how the current explicit mapping of mutations avoids this - no errors or warnings are given by the code below:
methods: {
clearUR () {
console.log('local method')
}
},
...mapMutations(['clearUR'])
The behaviour seems to be the opposite of using the mixin - i.e. the local clearUR() method is always called, whereas with the mixin approach, the store mutation is called instead. But it could still lead to confusion. In any case, that can be solved by convention - naming the mutations consistently for example.
Actually - ignore my comment about the behaviour being different - I think it's the same, and definitely confusing either way! In either case (explicit or implicit mapping), a warning would be useful perhaps?
It's actually not possible because the helpers do not have access to the component, they simply generate object that are merged with other objcets
In your example, the user explicitly write the same named method by herself. It is clear enough to notice her mistake in short time (may be just after writing them). But if mapMutations inject all mutations implicitly, it can conflict in every time she adds any mutation in the store since mapMutations also adds any new mutations without modifying the component codes. It would hard to notice and its effect is broad.
In any case, that can be solved by convention - naming the mutations consistently for example.
I think if we can avoid unwanted situation by restriction of system we should do that rather than make some rules.
But they do have access to the store? If so, mapAllGetters() would return an object based on the enumeration of the store getters, as my mixin does. I don't think they need access to the component - it would be used like this in the component itself:
computed: {
localThing () { // stuff },
...mapAllGetters()
or even, to avoid naming conflicts:
computed: {
localThing () { // stuff },
...mapAllGetters('state') // Now we can refer to this.state.anygetter
@ktsn You can avoid unwanted situations _by not using mapAllMutations()_ if you don't want to. But your rules and my rules differ. At the moment, I don't have a choice...
@posva Sorry - maybe you were referring to my point about issuing a warning for duplicate method names? In that case, yes, I can see that would be hard without access to the component.
@dsl101 @posva @ktsn Just to chime in here to possibly help, and also get some feedback on something I'm doing to get around this issue.
I also found injecting actions, getters, and state into my components was cumbersome. I agree with @ktsn that you can't simply inject all actions and mutations and getters into components due to namespacing issues. The assumptions about my project and implementation are as follows:
components only dispatch actions to the store. Having components commit mutations to the store
makes it harder to track bugs and know where state is being changed.
components only access data via getters. If components are accessing data via state they are aware of the structure of the store's state. By always creating getters to expose data, you are free to change around the store's internal state as much as you need without breaking the components.
All actions, getters, and mutations are global, i.e. I'm not using any namespaced modules.
For any given action, multiple store modules can react to it. i.e. module foo and module bar can have a thisThingHappened action. This make sense because for a given user interaction there could be a number of state changes required and it would be annoying to have dispatch a separate event to communicate with each part of the store, not to mention it will likely end up leading to a component knowing too much about a store's implementation.
All getters must be unique across all modules. This ensures that components don't accidentally end up getting the wrong data because of a namespace conflict.
All mutations must be unique. When a module makes a commit you should feel confident that it's only going to affect itself and not cause mutations in other stores. After all modules can always dispatch actions to other modules who can then make their own commits.
Implementation
Here is how I create my store
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import parseInterface from './parseInterface'
import assignToVueState from 'utils/assignToVueState'
Vue.use(Vuex)
const debug = process.env.NODE_ENV !== 'production'
const storeConfig = {
getters,
modules: {
...
},
plugins: [
...
],
strict: debug,
}
// parseInterface iterates over the store configuration and ensures that
// all getters and mutations are unique and returns an object that where the keys
// are all of the store's actions and the values are actions strings
// e.g. { thisThingHappened: 'thisThingHappened' }
storeConfig.state = {
Actions: parseInterface(storeConfig),
}
let store
if (process.env.NODE_ENV !== 'production') {
// This is block is for hot module reloading
storeConfig.mutations = {
updateActions (state, newActions) {
// assignToVueState is essentially Object.assign
assignToVueState(state.Actions, newActions)
},
}
store = new Vuex.Store(storeConfig)
if (module.hot) {
module.hot.accept([
...
], () => {
const newStoreConfig = {
getters: require('./getters').default,
modules: {
....
},
}
const newActions = parseInterface(newStoreConfig)
store.commit('updateActions', newActions)
store.hotUpdate(newStoreConfig)
})
}
} else {
store = new Vuex.Store(storeConfig)
}
export default store
Then I create a global mixin
import store from 'store'
Vue.mixin({
computed: {
$actions () {
const storeActions = {}
const Actions = store.getters.Actions
for (const action in Actions) {
storeActions[action] = params => {
return store.dispatch(action, params)
}
}
return storeActions
},
$getters () {
return store.getters
},
},
})
Now my components have access to every single getter and action.
vm.$actions.thisThingHappened()
// same as
vm.$store.dispatch('thisThingHappend')
vm.$getters.currentUser
Advantages of this strategy:
Sorry, that was long. Curious to hear what you guys think.
SIDE NOTE: I would use the module namespace feature to ensure the uniqueness of getters and mutations but it 1) I don't want actions to be namespaced and 2) Namespacing modules means that components have to know the state structure of a store. i.e. my/nested/module/foo.
SUGGESTION : Maybe the namespace feature could be improved by one allowing you to choose what parts of your module are namespaced (i.e. actions, mutations, or getters). Also, maybe instead of namespacing by the structure of the store, allow users to define their own namespace name and validate that the namespaces are unique when creating the store.
@trainiac I think you've also effectively implemented mapAllGetters('$getters') and mapAllActions('$actions') that I put in this comment, and then injected those via a mixin.
I think for large projects your principles around not accessing state directly are very sound. Sometimes it's just convenient though - mine is a fairly compact SPA, and using vuex is really to avoid passing what was 'App.vue'-level data objects down to child components as props.
And we both ended up iterating over all the actions / getters, etc. and not referring to them as a list in a component, which was my original gripe.
PS One advantage of having mapAllGetters(), etc. to use in a component (rather than doing it as a mixin as I and @trainiac are doing right now), is that _some_ components are just trivial UI things which don't need any access to state. They simply wouldn't map anything, and just rely on props and events. Using the mixin means everything is everywhere, which may not be a big issue, but I'm assuming these things aren't entirely without cost...
Yeah I hear different people having different concerns small vs large apps etc. I'm starting small but definitely plan to grow this app to a pretty big size.
I agree with @dsl101 that the API for vuex, while incredibly powerful (and awesome thank you team) it feels very geared towards the a mega application with many people working on it for time to concern themselves with the most efficient dependency injection.
If you end up doing the following:
The amount of cognitive load that is required to make a simple piece of data show up on the page seems to wear on you.
More importantly being able to quickly adjust your ideas and doing any of the following:
Means you have to go and update your components everywhere.
By having something like mapAllGetters and mapAllActions under a namespace it just feels like you avoid a ton of annoying development problems when you're trying to get things off the ground.
State or alternatives?
+1 on simplifying vuex, i think its currently weak spot.
usage like
$store.mutationname(parameters)
$store.actions.actionname(parameters)
supporting module architecture, without using strings - step one, maybe typed support in typescript - makes absolute sense for hole team. Its not laziness as much as just how to make it more clear, not using magic strings, be type safe and straightforward.
I like the already mentioned https://github.com/Raiondesu/Tuex or https://github.com/Glidias/haxevx projects that are already digging in that direction scaffold vuex for typed support, speed prototyping and whole development.
I hope after superb vue cli v3 came out (thank you youyuxi!) this ll be the next big thing how to use and scale vuex in bigger apps with those features.
I agree with this request. In my case, a ...mapState() that maps all props would be very helpful.
Same form me. I have lots of filter forms with 20+ inputs. I'm too lazy to write it :) again and again.
I'd like to suggest allowing mapping all state of an object in store, like this:
mapState("singletonComponents.login")
or:
mapState(state => state.pageStack[state.pageStack.length - 1])
I think it solves the issue of mapping everything, but still leaves the useful functionality of mapping all of component's state.
In a project I work on, we have objects that contain the state of exactly one component, so mapping all of it will not map a single extra property, and save quite a lot of typing.
Closing due to inactivity. I still agree with @posva and @ktsn that this can be done by mixin and such, though I don't think this amount of implicit behavior is desired as a best practice point of view.
Most helpful comment
I agree with this request. In my case, a
...mapState()that maps all props would be very helpful.