vuejs / vuex

🗃️ Centralized State Management for Vue.js.
https://vuex.vuejs.org
MIT License
28.4k stars 9.56k forks source link

Add useXXX helpers #1725

Open kiaking opened 4 years ago

kiaking commented 4 years ago

What problem does this feature solve?

Currently, we don't have mapXXX helpers equivalent feature when using Vuex 4 in composition api. It would be nice to have as both a convenience and for better typing support.

What does the proposed API look like?

To smoothly support migration from Vuex 3, at first, we should align with existing mapXXX helpers.

All of the following codes are meant to be used inside setup hook.

useState

const { count, countAlias, countPlusLocalState } = useState({
  // arrow functions can make the code very succinct!
  count: state => state.count,

  // passing the string value 'count' is same as `state => state.count`
  countAlias: 'count',

  // to access local state with `this`, a normal function must be used
  countPlusLocalState (state) {
    return state.count   this.localCount
  }
})

We should also support passing an array.

const { count } = useState([
  // map this.count to store.state.count
  'count'
])

useGetters

const { doneTodosCount, anotherGetter } = useGetters([
  'doneTodosCount',
  'anotherGetter'
])

Alias the name by passing an object.

const { doneCount } = useGetters({
  doneCount: 'doneTodosCount'
})

useActions

const { increment, incrementBy } = useActions([
  'increment', // map `increment()` to `store.dispatch('increment')`
  'incrementBy' // map `incrementBy(amount)` to `store.dispatch('incrementBy', amount)`
])

const { add } = useActions({
  add: 'increment' // map `add()` to `store.dispatch('increment')`
})

useMutations

const { increment, incrementBy } = useMutations([
  'increment', // map `increment()` to `store.commit('increment')`
  'incrementBy' // map `incrementBy(amount)` to `store.commit('incrementBy', amount)`
])

const { add } = useMutations({
  add: 'increment' // map `add()` to `store.commit('increment')`
})

Namespacing

All useXXX helpers should support passing namespace as the first argument.

const { a, b } = useState('some/nested/module', {
  a: state => state.a,
  b: state => state.b
})

const { foo, bar } = useActions('some/nested/module', [
  'foo',
  'bar'
])

And finally, useNamespacedHelpers.

const { useState, useActions } = useNamespacedHelpers('some/nested/module')

// look up in `some/nested/module`
const { a, b } = useState({
  a: state => state.a,
  b: state => state.b
})

// look up in `some/nested/module`
const { foo, bar } = useActions([
  'foo',
  'bar'
])

NOTE

There's an issue #1695 that proposes adding useModule helper that returns the whole module as an object. We could do the follow-up PR to tackle this idea as well.

lmiller1990 commented 4 years ago

An alternative would be to provide these in a separate library, and users could use to include that, too, if you want to keep core very small and simple.

I like the proposal in general - it mirrors the mapXXX helpers nicely.

kiaking commented 4 years ago

An alternative would be to provide these in a separate library, and users could use to include that, too, if you want to keep core very small and simple.

Ah good point. Maybe something we should think about when designing Vuex 5. As of Vuex 4, mapXXX is already in the core and to align with Vuex 3, I think it makes more sense to have it in the core 👍

libbGit commented 4 years ago

in vuex 4.0.0-beta.2, I don't find useState, just mapState has been given, how do i use mapState it?

libbGit commented 4 years ago

An alternative would be to provide these in a separate library, and users could use to include that, too, if you want to keep core very small and simple.

Ah good point. Maybe something we should think about when designing Vuex 5. As of Vuex 4, mapXXX is already in the core and to align with Vuex 3, I think it makes more sense to have it in the core 👍

how do i use mapXXX?

in the setup function, i can't get the store state.

import { mapState, mapMutations, mapGetters, mapActions } from "vuex"; 
setup(props, context) { 
     let state = mapState("user",["name"]); 
     // state.name is a mappedState function, not a value
}
kiaking commented 4 years ago

You can't use mapXXX helpers inside setup hook. And useXXX helpers are not there too. Please wait until this issue is being tackled! 🙇

libbGit commented 4 years ago

You can't use mapXXX helpers inside setup hook. And useXXX helpers are not there too. Please wait until this issue is being tackled! 🙇

okay, thx, Wishing good news will come soon.

PatrykWalach commented 4 years ago

this is pretty easy to implement, mappedActions have to be bound with $store

const mappedActions = mapActions('user', ['login'])
const store = useStore()
const login = mappedActions.login.bind({ $store: store })

useActions can be created like a so

const useActions = (...args) => {
  const $store = useStore()

  return Object.fromEntries(
    Object.entries(mapActions(...args)).map(
      ([key, value]) => [
        key,
        value.bind({
          $store,
        }),
      ],
    ),
  )
}

and if you use typescript you have to cast all the overloaded types

const useActions = ((...args: [any, any]) => {
  const $store = inject('store')

  return Object.fromEntries(
    (Object.entries(mapActions(...args))).map(
      ([key, value]: [string, ActionMethod]) => [
        key,
        value.bind({
          $store,
        }),
      ],
    ),
  )
}) as Mapper<ActionMethod> &
  MapperWithNamespace<ActionMethod> &
  MapperForAction &
  MapperForActionWithNamespace

the same probably goes for mapMutations

mappedGetters and mappedState in vue 2 work straight up, since this in computed is bound to the instance.

 const getters = mapGetters('updateManager', ['rejected', 'isUpdating'])
 const rejected = computed(getters.rejected)

But if it's necessary in vue 3, this can be bound as well.

 const getters = mapGetters('updateManager', ['rejected', 'isUpdating'])
 const rejected = computed(getters.rejected.bind({ $store: store }))

So useGetters would look like this:

const useActions = (...args) => {
  const $store = useStore()

  return Object.fromEntries(
    Object.entries(mapGetters(...args)).map(
      ([key, value]) => [
        key,
        computed(value.bind({
          $store,
        })),
      ],
    ),
  )
}

and this should be casted with as Mapper<ComputedRef<any>> & MapperWithNamespace<ComputedRef<any>>

All the hooks ```typescript import { mapGetters, mapState, Mapper, MapperWithNamespace, MapperForState, MapperForStateWithNamespace, Computed, MapperForActionWithNamespace, MutationMethod, mapMutations, mapActions, MapperForMutationWithNamespace, MapperForMutation, ActionMethod, MapperForAction, } from 'vuex' import { ComputedRef, computed, inject } from '@vue/composition-api' const createActionHook = ( mapFunction: Mapper & MapperWithNamespace, ) => ((...args: [any, any]) => { const $store = inject('store') return Object.fromEntries( Object.entries(mapFunction(...args)).map( ([key, value]: [string, any]) => [ key, value.bind({ $store, }), ], ), ) }) as Mapper & MapperWithNamespace export const useMutation = createActionHook(mapMutations) as Mapper< MutationMethod > & MapperWithNamespace & MapperForMutation & MapperForMutationWithNamespace export const useActions = createActionHook(mapActions) as Mapper & MapperWithNamespace & MapperForAction & MapperForActionWithNamespace const createComputedHook = ( mapFunction: Mapper & MapperWithNamespace, ) => ((...args: [any, any]) => { const $store = inject('store') return Object.fromEntries( Object.entries(mapFunction(...args)).map( ([key, value]: [string, Computed]) => [ key, computed( value.bind({ $store, }), ), ], ), ) }) as Mapper> & MapperWithNamespace> export const useGetters = createComputedHook(mapGetters) as Mapper< ComputedRef > & MapperWithNamespace> export const useState = createComputedHook(mapState) as Mapper< ComputedRef > & MapperWithNamespace> & MapperForState & MapperForStateWithNamespace ```
Stoom commented 4 years ago

vuex-composition-helpers has a good implementation of this and support for typescript type interfaces

shawnwildermuth commented 4 years ago

The vuex-composition-helpers project only works with Vue 2 as it was said that this is what the API would look like in Vuex 4 but I haven't seen it working.

Stoom commented 4 years ago

I guess my point was the above examples is still missing type safety. This is Paramount to preventing bugs around the store. The library suggested was to demonstrate how you can have type safety... Better yet is a bird architecture that puts types first from the beginning so some crazy advanced typing isn't required.

shawnwildermuth commented 4 years ago

Wouldn't an API like this allow for typesafety:

setup() {
  const { isBusy, count } = useState({
    isBusy: s => s.state.isBusy,
    count: s => s.getters.itemCount  
  });
}

Maybe it's too verbose.

Stoom commented 4 years ago

The problem in the current API is vuex is wrapping things. So you define an action it takes in the action context and the payload. When you use it you only provide a payload and vuex magically fills in the action context. Same for getters and the state argument, and mutations and their arguments.

shawnwildermuth commented 4 years ago

Hmm....so is this an argument against Vuex in general?

Stoom commented 4 years ago

Kinda yeah, but I don't know a better way of supporting everything. Wishlist it would be nice to have fully typed commit and dispatch functions too.

Maybe having a more direct access to the module and make a module aware of all state, getters, mutations, and actions, as they would be used from a component. Then we could simply map the module and interact with it like any other class...

shawnwildermuth commented 4 years ago

If all we're getting is protection against accidental mutation, couldn't we do that with just ref/reactive? Thinking out loud. I really like the simplicity of Vuex and don't think that type safety as as big of an issue for me.

Stoom commented 4 years ago

Yeah I mean with "hooks" and refs you could mimic vuex. As for type safety, we've found numerous bugs in or code where one developer assumed the store was one way, or during a refactor changed the interface.

shawnwildermuth commented 4 years ago

Sure, I get that. I don't mean hooks. I don't quite get useStore except for library code (import store from "@/store" is fine for most cases I think).

Stoom commented 4 years ago

Possibly things like Nuxt where you don't have direct access to the store? #blackmagic 🤢

PatrykWalach commented 4 years ago

I think the biggest issues with type safety are:

  1. typescript not having optional type arguments It's impossible to just pass type of state
    useState<UserState>('user', ['name', 'id'])

    The second type has to be passed as well

    useState<UserState, 'name' | 'id'>('user', ['name', 'id'])
    useState<UserState, { name(state: UserState): string, id: string }>('user', { name: state => state.name, id: 'id' })

    The only way around this is to create a thunk

    
    const { useState: useUserState } = createNamespacedHooks<UserState, UserGetters>('user')

useUserState(['name', 'id'])

I create the `./helpers` file where I store all hooks to all modules to avoid creating them in multiple components

2. dispatch and commit
They can be easily typed with a pattern used in redux
```tsx
const createAction = <P>(type: string) => {
  const action = (payload: P) => ({ type, payload})
  action.toString = () => type
  return action
}

const fetchUserData = createAction<{ userId: number }>('fetchUserData ')

const actions = {
  [fetchUserData](_, { userId }: { userId: string }){
  ...
  }
}

dispatch(fetchUserData({ userId: 2 }))

it's also possible to create a function that would type the payload inside of action tree

const actions = createActions((builder) => builder
  .addAction(fetchUserData, (context, { userId }) => {
    ...
  })
)
  1. getters not being typed I worked around this like that
    
    interface UserGetters {
    isLoggedIn: boolean
    }

type UserGetterTree = CreateGetterTree<UserState, RootState, RootGetters, UserGetters>

const getters: UserGetterTree = { isLoggedIn: (state) => !!state.name }


then I can use `UserGetters` to type the hooks
Stoom commented 4 years ago

This example shows how it's difficult for a developer to keep type safety, vs just having a simple solution without a lot of boilerplate. Even in commit/dispatch example there's a typing error where userId was typed as a number in the crate, but then a string when declaring the actual function.

const​ ​createAction​ ​=​ ​<​P​>​(​type​: ​string​)​ ​=>​ ​{​
  ​const​ ​action​ ​=​ ​(​payload​: ​P​)​ ​=>​ ​(​{​ type​,​ payload​}​)​
  ​action​.​toString​ ​=​ ​(​)​ ​=>​ ​type​
  ​return​ ​action​
​}​

​const​ ​fetchUserData​ ​=​ ​createAction​<​{​ ​userId​: ​number​ ​}​>​(​'fetchUserData '​)​

​const​ ​actions​ ​=​ ​{​
  ​[​fetchUserData​]​(​_​,​ ​{​ userId ​}​: ​{​ ​userId​: ​string​ ​}​)​{​
  ...
  ​}​
​}​

​dispatch​(​fetchUserData​(​{​ ​userId​: ​2​ ​}​)​)
Stoom commented 4 years ago

Maybe the typing discussion should be moved to a different issue?

petervmeijgaard commented 3 years ago

It's been more than a month already. Is there an update on this ticket? I'd really love to have first-party support for these useX-hooks. It'll clean up my project quite some bit.

ux-engineer commented 3 years ago

I've been following this issue, and as there have been discussion about type-safety with Vuex 4, I'd like to add my summary of some problem points along with an example repo of how to type Vuex 4 store... (Feel free to mark this as off-topic, if so.)

xiaoluoboding commented 3 years ago

Recently, I implement the all things in this issue talk about, and I'm consider about to contribute in vuex, then i found that, the useXXX helpers proposal already exist nearly eight months.

checkout https://github.com/vueblocks/vue-use-utilities#vuex

@vueblocks/vue-use-vuex - Use Vuex With Composition API Easily. It build on top of vue-demi & @vue/compostion-api. It works both for Vue 2 & 3, TypeScript Supported too.

useVuex

useStore

Usage

useState

import { useVuex } from '@vueblocks/vue-use-vuex'

export default {
  // ...
  setup () {
    // Use the useState as you would use mapState
    const { useState } = useVuex()

    return {
      // mix this into the outer object with the object spread operator
      ...useState({
        // arrow functions can make the code very succinct!
        count: state => state.count,

        // passing the string value 'count' is same as `state => state.count`
        countAlias: 'count',

        // to access local state with `this`, a normal function must be used
        countPlusLocalState (state) {
          return state.count + this.localCount
        }
      }),
      ...mapState([
        // map count<ComputedRef> to store.state.count
        'count'
      ])
    }
  }
}

useGetters

import { useVuex } from '@vueblocks/vue-use-vuex'

export default {
  // ...
  setup () {
    // Use the useGetters as you would use mapGetters
    const { useGetters } = useVuex()

    return {
      // mix the getters into outer object with the object spread operator
      ...useGetters([
        'doneTodosCount',
        'anotherGetter',
        // ...
      ]),
      // if you want to map a getter to a different name, use an object:
      ...mapGetters({
        // map `doneCount<ComputedRef>` to `this.$store.getters.doneTodosCount`
        doneCount: 'doneTodosCount'
      })
    }
  }
}

useMutations

import { useVuex } from '@vueblocks/vue-use-vuex'

export default {
  // ...
  setup () {
    // Use the useMutations as you would use mapMutations
    const { useMutations } = useVuex()

    return {
      ...useMutations([
        'increment', // map `increment()` to `this.$store.commit('increment')`

        // `mapMutations` also supports payloads:
        'incrementBy' // map `incrementBy(amount)` to `this.$store.commit('incrementBy', amount)`
      ]),
      ...useMutations({
        add: 'increment' // map `add()` to `this.$store.commit('increment')`
      })
    }
  }
}

useActions

import { useVuex } from '@vueblocks/vue-use-vuex'

export default {
  // ...
  setup () {
    // Use the useActions as you would use mapActions
    const { useActions } = useVuex()

    return {
      ...useActions([
        'increment', // map `increment()` to `this.$store.dispatch('increment')`

        // `mapActions` also supports payloads:
        'incrementBy' // map `incrementBy(amount)` to `this.$store.dispatch('incrementBy', amount)`
      ]),
      ...useActions({
        add: 'increment' // map `add()` to `this.$store.dispatch('increment')`
      })
    }
  }
}

namespacing

also support

// Get namespaced component binding helpers in useVuex
import { useVuex } from '@vueblocks/vue-use-vuex'

export default {
  setup () {
    const { mapState, mapActions } = useVuex('some/nested/module')

    return {
      // look up in `some/nested/module`
      ...mapState({
        a: state => state.a,
        b: state => state.b
      })
      // look up in `some/nested/module`
      ...mapActions([
        'foo',
        'bar'
      ])
    }
  }
}

It seems familiar right? Yeah, You could think of @vueblocks/vue-use-vuex as a wrapper of Vuex Helpers

Read Docs

But, I'm didn't think too much about type safety, and i am still learning TypeScript. If you're interested it, Please help me improve it.

PRs Welcome in @vueblocks/vue-use-utilities

Alanscut commented 3 years ago

I find the proposal has been stalled for a long time.Is it still under development?Hope this proposal can be realised soon.

hi-reeve commented 3 years ago

i hope this will be implemented very soon

Fanna1119 commented 3 years ago

Any update on this?

hi-reeve commented 3 years ago

there is another package like this, maybe you can check this package

https://github.com/vueblocks/vue-use-utilities

ux-engineer commented 3 years ago

I think they are focused on Vuex version 5 RFC, which is totally new syntax for Vuex, similar to Composition API:

https://github.com/kiaking/rfcs/blob/vuex-5/active-rfcs/0000-vuex-5.md

https://github.com/vuejs/rfcs/discussions/270

towertop commented 3 years ago

I implemented similar useXXX helpers during an experimental migration for my team's project. It is a single ts source file that you can copy and use. But the code pattern is just my personal opinion and maybe not quite helpful.

Plz check out the code and demo from https://github.com/towertop/vuex4-typed-method .

yulafezmesi commented 3 years ago

One of the big deficiencies for vuex, any update on this?

jaitaiwan commented 3 years ago

There's already a PR handling this, just needs to be merged by the looks of things

vnues commented 2 years ago

https://github.com/greenpress/vuex-composition-helpers

this can be satisfied

asasugar commented 2 years ago

https://github.com/asasugar/vuex-composition-maphooks 【modified to vuex helpers】

michaelnwani commented 2 years ago

A note for travelers whom somehow find their way here: the community has moved on to Pinia as the official Vue store: https://vuejs.org/guide/scaling-up/state-management.html#pinia

asasugar commented 2 years ago

https://github.com/asasugar/vuex-composition-maphooks 【modified to vuex helpers】

I have switched to Pinia