vuex
vuex copied to clipboard
Add useXXX helpers
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.
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.
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 👍
in vuex 4.0.0-beta.2, I don't find useState, just mapState has been given, how do i use mapState it?
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,
mapXXXis 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
}
You can't use mapXXX helpers inside setup hook. And useXXX helpers are not there too. Please wait until this issue is being tackled! 🙇
You can't use
mapXXXhelpers insidesetuphook. AnduseXXXhelpers are not there too. Please wait until this issue is being tackled! 🙇
okay, thx, Wishing good news will come soon.
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
vuex-composition-helpers has a good implementation of this and support for typescript type interfaces
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.
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.
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.
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.
Hmm....so is this an argument against Vuex in general?
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...
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.
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.
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).
Possibly things like Nuxt where you don't have direct access to the store? #blackmagic 🤢
I think the biggest issues with type safety are:
- 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
- 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 }) => {
...
})
)
- 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
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 }))
Maybe the typing discussion should be moved to a different issue?
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.
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.)
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 asmapStateuseGetters- same asmapGettersuseMutations- same asmapMutationsuseActions- same asmapActions
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
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
I find the proposal has been stalled for a long time.Is it still under development?Hope this proposal can be realised soon.
i hope this will be implemented very soon
Any update on this?
there is another package like this, maybe you can check this package
https://github.com/vueblocks/vue-use-utilities
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
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 .
One of the big deficiencies for vuex, any update on this?
There's already a PR handling this, just needs to be merged by the looks of things
https://github.com/greenpress/vuex-composition-helpers
this can be satisfied
https://github.com/asasugar/vuex-composition-maphooks 【modified to vuex helpers】
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
https://github.com/asasugar/vuex-composition-maphooks 【modified to vuex helpers】
I have switched to Pinia