Vuex: Discussion on removing mapXXX helpers in Vuex 4

Created on 11 Oct 2018  路  14Comments  路  Source: vuejs/vuex

@yyx990803 @ktsn Regarding this note in the current roadmap for Vuex 4:

Getting rid of mapXXX helpers via scoped-slot based store consumer component

I've experimented with this scoped-slot pattern in some apps, but it has a couple serious limitations that have led to me never using it anymore:

  • Vuex state/getters/actions are only conveniently available in templates and render functions. That means the convenience is unavailable for computed properties that rely on Vuex state and methods that dispatch Vuex actions, which is a big limitation.

  • It requires tight coupling of components to Vuex modules, which I've found doesn't scale well to large apps. Instead, I've found it very useful to _never_ use the mapXXX helpers directly in my components, but in a state helpers file, which describes the public interfaces for all Vuex modules. From this file, I'll import the concerns I care about into individual components. For example:

    import {
    authComputed,
    notificationsComputed,
    notificationsMethods,
    searchMethods,
    } from '@state/helpers'
    
    // ...
    
    computed: {
    ...authComputed,
    ...notificationsComputed,
    },
    methods: {
    ...notificationsMethods,
    ...searchMethods,
    },
    

    This prevents verbose and duplicated mapXXX code in components, replaced by a nice list of the global concerns this component cares about. But the benefit _really_ comes in when adding new state/getters/actions or when refactoring a Vuex module. For example, if I add a new userLoggedIn getter to authComputed, it will automatically be available in any component using authComputed. Or, if I rename some state or split it out into additional modules, the state helpers file allows the refactor to be done in two steps: first on the Vuex side, while updating the state helpers for compatibility, _then_ in my components (if I actually want the public interface to change as well).

For these reasons, I feel like the mapXXX helpers should stay and the scoped slot pattern should actually be avoided. What are others' thoughts? Is there something I've missed?

discussion

Most helpful comment

@leoyli

I'm not sure if I get your points. In the roadmap just saying it is considered to combine (?) the mutation and actions, but did not saying how or points a direction.

When the roadmap says they'll be "combined", it's just referring to code like state.foo = 'bar' moving to actions, since the job of mutations would now be done automatically through the reactivity system. 馃檪

the point is the conceptual architecture of Vuex is inspired by Flux, i.e. the predictable uni-directional data flow (and that is why often these libraries are design to encourage functional paradigm). Thus, we need something like reducer or mutation to tell the store how to update its state (so it has to be a sync function). To my understanding, the reason we have actions and mutations is just a separation of concerns.

Flux, functional, immutable, etc - these are strategies, but not the goal. The goal is to be able to log, describe, and replay state changes to facilitate debugging and prototyping complex state.

Previously, mutations were used to describe and replay state changes, but the reactivity system can do both of these automatically. Instead of having to explicitly create and name a mutation SET_TODOS or PUSH_TODO, we can just detect state.todos = [] or state.todos.push(todo) and show you something like todos (SET) or todos (PUSH), because we can deduce the name of the state that was mutated _and_ the kind of mutation. Then, because we have access to _what_ was changed and exactly _how_ it was changed, replaying those state changes becomes very simple.

So by embracing mutable, but reactive state, we can actually achieve the same goals, but with fewer concepts, simpler code, and less boilerplate.

In React/Redux, you can not mutate the state out of a reducer, but In Vue/Vuex, you actually can mutate the state of out of mutations.

Mutating state outside of a reducer _is_ actually possible with Redux, unless you're using immutable data (e.g. with immutable.js or immer). Unlike Redux though, Vue actually logs a warning if users try to mutate state outside of mutations, making it more difficult to do accidentally. Enabling strict mode makes it impossible, throwing an error if you try. 馃槈

All 14 comments

Thanks for opening this discussion @chrisvfritz!

I'm actually thinking mapXXX helpers should be kept too. As a TypeScript user, there may be a way to make Vuex more type safe by using the helpers (like #1121). In addition, when we achieve template type checking, mapped computed / methods are probably able to check but checking the values which are passed via scoped slot seems hard.

I guess the intention of scoped-slot based approach is that it forces the users to write application logic in the store so that the component will be just the reflection of the store state. But I think there are some cases that we want to write some logic in components. If there are something I'm missing about the approach, I'd like to know. (I didn't actually experience the approach yet)

PLZ keep mapXXX helpers too, here is how I used it:

  • I use namespaced modules. My module is carefully design to avoid cross-module coupling. (i.e. mutations/actions never mutate states or dispatch action in other modules. I call store.subscribe if I really have to dispatch other module's actions.)
  • I store types in constants.

All my modules have this boilerplate:

const moduleName = `the_name_of_module`
const namespaced = true
const types = {
  SOME_ACTION: 'SOME_ACTION',
  ...
}

// (the objects store getters, mutations and actions)
...
...
...

// EXPORTS
export { types }

export const {
  mapState,
  mapGetters,
  mapActions,
} = createNamespacedHelpers(moduleName)

export default {
  moduleName,
  namespaced,
  state,
  getters,
  mutations,
  actions,
}

And here is how I use in the component

<script>
import { types, mapState, mapGetters, mapActions } from '@/store-modules/some-module'

export default {
  computed: {
    ...mapGetters(['someGetter']),
  },
  methods: {
    ...mapActions({
      methodName: types.SOME_ACTION,
    }),
  },
}
</script>

As you can see here is a bit boilerplate code (but not too much); however, it is very explicit to trace where the action is mapping from, and can be scaled very well, especially when new people being added to our project. I'm also getting helped by the IDE to map my functions thanks to static analysis.

PS1. I'm only calling actions in components, I don't like the idea of calling mutation since it just confusing, so no mapMutations in my use case. And this way I can manage subscriptions easier.

PS2. I think it is also possible to further abstract the module boilerplate, but that is my current approach for now.

I'm only calling actions in components, I don't like the idea of calling mutation since it just confusing, so no mapMutations in my use case. And this way I can manage subscriptions easier.

For the record, I also never use mapMutations (I see it as an anti-pattern), but this will be moot since mutations will no longer exist in Vuex 4. 馃帀馃檪

I'm not sure what the what __Getting rid of the need for separating actions and mutations__ means. We still need to update the state, and since the way current Vue works, I prefer to have mutation stay in sync, while action in async. To me action is some middleware like in the redux-thunk, and mutation is sort of the reducer in redux fashion.

I believe Vuex should only working on states not anything-else. So far I can elegantly decouple Vuex and Vue-router via subscribe and subscribeActions, I still need mutation to kick off some reactivity in subscribe though, since subscribeActions is called prior to the action completion but subscribe is called after state being updated. And that is another confusing part, since there are no subscribeMutations as its counter-pair, and these two subscribers are working a bit differently.

@leoyli The main reason we need mutations now is so that we can capture and label mutations for logging, plus debugging and prototyping via time travel in the devtools. However, with Vue's reactivity system, it's theoretically possible to capture and label state changes automatically. That way, actions can mutate state directly without losing data in the devtools - though improvements are also planned here actually, including an async timeline of actions. 馃檪 Does that make sense?

@chrisvfritz I'm not sure if I get your points. In the roadmap just saying it is considered to combine (?) the mutation and actions, but did not saying how or points a direction. As for the devTool, I have no idea about its implementations.

I know the way Vue tract the state changing making it is powerful and reactive and what you've said is theoretically possible, but the point is the conceptual architecture of Vuex is inspired by Flux, i.e. the predictable uni-directional data flow (and that is why often these libraries are design to encourage functional paradigm). Thus, we need something like reducer or mutation to tell the store how to update its state (so it has to be a sync function). To my understanding, the reason we have actions and mutations is just a separation of concerns.

Also, due to this separation of concerns, that is the reason why the doc tell us only mutation is suppose to mutate states. In React/Redux, you can not mutate the sate out of a reducer, but In Vue/Vuex, you actually can mutate the state of out of mutations. However, you can dose not mean you should. Although I can see here that Vue is more align to "reactive" and less "functional" programming style, my statement might not be that relevant.

(I sometime think that React and Vue should swap their names, haha)

@leoyli

I'm not sure if I get your points. In the roadmap just saying it is considered to combine (?) the mutation and actions, but did not saying how or points a direction.

When the roadmap says they'll be "combined", it's just referring to code like state.foo = 'bar' moving to actions, since the job of mutations would now be done automatically through the reactivity system. 馃檪

the point is the conceptual architecture of Vuex is inspired by Flux, i.e. the predictable uni-directional data flow (and that is why often these libraries are design to encourage functional paradigm). Thus, we need something like reducer or mutation to tell the store how to update its state (so it has to be a sync function). To my understanding, the reason we have actions and mutations is just a separation of concerns.

Flux, functional, immutable, etc - these are strategies, but not the goal. The goal is to be able to log, describe, and replay state changes to facilitate debugging and prototyping complex state.

Previously, mutations were used to describe and replay state changes, but the reactivity system can do both of these automatically. Instead of having to explicitly create and name a mutation SET_TODOS or PUSH_TODO, we can just detect state.todos = [] or state.todos.push(todo) and show you something like todos (SET) or todos (PUSH), because we can deduce the name of the state that was mutated _and_ the kind of mutation. Then, because we have access to _what_ was changed and exactly _how_ it was changed, replaying those state changes becomes very simple.

So by embracing mutable, but reactive state, we can actually achieve the same goals, but with fewer concepts, simpler code, and less boilerplate.

In React/Redux, you can not mutate the state out of a reducer, but In Vue/Vuex, you actually can mutate the state of out of mutations.

Mutating state outside of a reducer _is_ actually possible with Redux, unless you're using immutable data (e.g. with immutable.js or immer). Unlike Redux though, Vue actually logs a warning if users try to mutate state outside of mutations, making it more difficult to do accidentally. Enabling strict mode makes it impossible, throwing an error if you try. 馃槈

@chrisvfritz

Wow, thank you for clarifications here. Now I see the picture. Indeed, to define the mutation types that not used in anywhere else (except in store.subscribe) is a bit cumbersome and full of boilerplate. I think people coming from React (like me) are just accepting the concept of immutable and functional paradigm, but thinking it carefully, reaction + mutable is much intuitive combination.

Maybe a bit off topic, I do agree functional is just a strategy; but walk way from the concept of pure functions cause me some concerns about testability. I can see now we are more or less testing the side effects, is reactive system can also play a role here?!

Back to the topic, another arguable point here is the magic string. Often, I found it is almost everywhere in Vue, making it is (1) easy to make typos, (2) hard to trace where the function is coming from. And that is why I also much prefer more explicit approach, mapXXX helps me to map getters/actions from a module (like the one in your state helper file), and bind the method in computed/methods with a short version of names.

I do agree functional is just a strategy; but walk way from the concept of pure functions cause me some concerns about testability. I can see now we are more or less testing the side effects, is reactive system can also play a role here?!

@leoyli Actions can still be tested the same way it was possible to test mutations, e.g.:

const state = { foo: 'bar' }
const newFoo = 'baz'
myModule.actions.updateFoo({ state }, newFoo) 
expect(state.foo).toEqual(newFoo)

Which _is_ testing side effects, but it's not harder than if actions were pure functions that returned a new state object, like this:

const state = { foo: 'bar' }
const newFoo = 'baz'
const newState = myModule.actions.updateFoo({ state }, newFoo) 
expect(newState.foo).toEqual(newFoo)

And you can still break down logic into as many separate modules, exported variables, or pure functions as you want where it helps with testability. Vue's reactivity system - and even Vue and Vuex themselves - don't have to be involved at all when testing Vuex modules.

@chrisvfritz

Thanks a lot! Ok I now finally there. If I were going to upgrade Vuex 3 to 4, it sounds like I just simply move all my mutations under actions, and replace commit as dispatch. Everything would be the same and no more confusion between mutations and actions. The next thing I have to look after is the place I used commit, since it now return a promise.

Getting rid of mapXXX helpers via scoped-slot based store consumer component

Are there any references to describe what this API might look like?

Personally, I find scoped slots in Vue to be very awkward. I've found that using provide/inject can often achieve similar purposes with more readability[1] when components are designed to be used together in parent/child relationships (similar to Context in React). This feels like a natural strategy for shared Vuex state also.

[1] (except that making provided values reactive is a bit awkward)

The idea of having both mapState and mapGetters is nuts! 馃槄 Vue doesn't distinguish between computed properties and data attributes and neither should vuex. When we're _reading_ data we really don't care... it's like going back to before getters existed and differentiating between .property and .property().

Closing due to inactivity. This is interesting discussion regarding next iteration of Vuex, though I see people referencing this issue and sees it like this is happening. It might, but because of its age, I think we should start a new discussion on how next Vuex iteration would be when the time comes.

Was this page helpful?
0 / 5 - 0 ratings