vuex icon indicating copy to clipboard operation
vuex copied to clipboard

Add useXXX helpers

Open kiaking opened this issue 5 years ago • 36 comments

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.

kiaking avatar Apr 25 '20 06:04 kiaking

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.

lmiller1990 avatar Apr 28 '20 23:04 lmiller1990

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 👍

kiaking avatar Apr 30 '20 01:04 kiaking

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

libbGit avatar Jun 02 '20 07:06 libbGit

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
}

libbGit avatar Jun 02 '20 08:06 libbGit

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

kiaking avatar Jun 02 '20 09:06 kiaking

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.

libbGit avatar Jun 02 '20 10:06 libbGit

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
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<any> & MapperWithNamespace<any>,
) =>
((...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<any> & MapperWithNamespace<any>

export const useMutation = createActionHook(mapMutations) as Mapper<
MutationMethod
> &
MapperWithNamespace<MutationMethod> &
MapperForMutation &
MapperForMutationWithNamespace

export const useActions = createActionHook(mapActions) as Mapper<ActionMethod> &
MapperWithNamespace<ActionMethod> &
MapperForAction &
MapperForActionWithNamespace

const createComputedHook = (
mapFunction: Mapper<Computed> & MapperWithNamespace<Computed>,
) =>
((...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<ComputedRef<any>> & MapperWithNamespace<ComputedRef<any>>

export const useGetters = createComputedHook(mapGetters) as Mapper<
ComputedRef<any>
> &
MapperWithNamespace<ComputedRef<any>>

export const useState = createComputedHook(mapState) as Mapper<
ComputedRef<any>
> &
MapperWithNamespace<ComputedRef<any>> &
MapperForState &
MapperForStateWithNamespace

PatrykWalach avatar Sep 04 '20 13:09 PatrykWalach

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

Stoom avatar Sep 07 '20 21:09 Stoom

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.

shawnwildermuth avatar Sep 08 '20 23:09 shawnwildermuth

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.

Stoom avatar Sep 09 '20 02:09 Stoom

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.

shawnwildermuth avatar Sep 09 '20 22:09 shawnwildermuth

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.

Stoom avatar Sep 09 '20 22:09 Stoom

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

shawnwildermuth avatar Sep 09 '20 22:09 shawnwildermuth

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

Stoom avatar Sep 09 '20 22:09 Stoom

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.

shawnwildermuth avatar Sep 09 '20 23:09 shawnwildermuth

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.

Stoom avatar Sep 09 '20 23:09 Stoom

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

shawnwildermuth avatar Sep 09 '20 23:09 shawnwildermuth

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

Stoom avatar Sep 10 '20 02:09 Stoom

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

  1. dispatch and commit They can be easily typed with a pattern used in redux
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

PatrykWalach avatar Sep 10 '20 09:09 PatrykWalach

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 avatar Sep 10 '20 10:09 Stoom

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

Stoom avatar Sep 10 '20 10:09 Stoom

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.

petervmeijgaard avatar Oct 12 '20 11:10 petervmeijgaard

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

ux-engineer avatar Oct 12 '20 12:10 ux-engineer

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

  • useState - same as mapState
  • useGetters - same as mapGetters
  • useMutations - same as mapMutations
  • useActions - same as mapActions

useStore

  • useStore - same as Vuex 4.x composition api 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

xiaoluoboding avatar Dec 08 '20 06:12 xiaoluoboding

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

Alanscut avatar Dec 28 '20 06:12 Alanscut

i hope this will be implemented very soon

hi-reeve avatar Jan 06 '21 10:01 hi-reeve

Any update on this?

Fanna1119 avatar Mar 12 '21 09:03 Fanna1119

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

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

hi-reeve avatar Mar 12 '21 09:03 hi-reeve

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

ux-engineer avatar Mar 12 '21 17:03 ux-engineer

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 .

towertop avatar May 18 '21 02:05 towertop

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

yulafezmesi avatar Jun 16 '21 07:06 yulafezmesi

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

jaitaiwan avatar Jul 02 '21 13:07 jaitaiwan

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

this can be satisfied

vnues avatar Sep 29 '21 08:09 vnues

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

asasugar avatar Jan 27 '22 10:01 asasugar

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

michaelnwani avatar Apr 06 '22 04:04 michaelnwani

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

I have switched to Pinia

asasugar avatar Apr 08 '22 10:04 asasugar