Skip to content

Discussion on removing mapXXX helpers in Vuex 4 #1417

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
chrisvfritz opened this issue Oct 11, 2018 · 14 comments
Closed

Discussion on removing mapXXX helpers in Vuex 4 #1417

chrisvfritz opened this issue Oct 11, 2018 · 14 comments

Comments

@chrisvfritz
Copy link
Collaborator

chrisvfritz commented Oct 11, 2018

@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?

@chrisvfritz chrisvfritz changed the title Discussion on removing mapXXX helpers in Vuex 4 Discussion on removing mapXXX helpers in Vuex 4 Oct 11, 2018
@ktsn
Copy link
Member

ktsn commented Oct 13, 2018

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)

@leoyli
Copy link

leoyli commented Oct 13, 2018

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.

@chrisvfritz
Copy link
Collaborator Author

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. 🎉🙂

@leoyli
Copy link

leoyli commented Oct 13, 2018

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.

@chrisvfritz
Copy link
Collaborator Author

@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?

@leoyli
Copy link

leoyli commented Oct 14, 2018

@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)

@chrisvfritz
Copy link
Collaborator Author

chrisvfritz commented Oct 14, 2018

@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. 😉

@leoyli
Copy link

leoyli commented Oct 14, 2018

@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.

@chrisvfritz
Copy link
Collaborator Author

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.

@leoyli
Copy link

leoyli commented Oct 15, 2018

@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.

@indirectlylit
Copy link

indirectlylit commented Jan 18, 2019

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)

@ryanmtaylor
Copy link

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().

@primaryperson
Copy link

Maybe vuex-class-component?

@kiaking
Copy link
Member

kiaking commented Apr 23, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants