Hami melon flavored Vuex! State management for Vue.js
Features:
- Build on Vuex, compatible with Vuex 3 & 4
- Also compatible with Vue 2 and Vue 3
- Modular by design, no more fat store
- Completely TypeScript intelligence support
- Unit tests line coverage: 100%
npm install --save hami-vuex
// store/index.js
import { createHamiVuex } from 'hami-vuex'
export const hamiVuex = createHamiVuex({
vuexStore: /* the Vuex Store instance, created according to Vuex docs */
})
Or use the default empty Vuex Store instance, Vue 2 + Vuex 3 Writing:
import Vue from 'vue'
import Vuex from 'vuex'
import { createHamiVuex } from 'hami-vuex'
Vue.use(Vuex)
export const hamiVuex = createHamiVuex({
/* Optional:Vuex Store constructor options */
})
And Vue 3 + Vuex 4 Writing:
import { createApp } from 'vue'
import { createHamiVuex } from 'hami-vuex'
export const hamiVuex = createHamiVuex({
/* Optional:Vuex Store constructor options */
})
export const app = createApp()
app.use(hamiVuex)
The Hami Vuex instance is for create and manage Hami Store instances, all business logics should implemented by Hami Store instances.
// store/counter.js
import { hamiVuex } from '@/store/index'
// Internal:dynamic registered Vuex module
export const counterStore = hamiVuex.store({
// unique name of the store, will appear in devtools
$name: 'counter',
// define the state with plain object(will auto deep copy it)
$state: {
count: 0,
},
// or a function that returns a fresh state
$state() {
return { count: 0 }
}
// define a getter, similar to Vue computed
get double() {
return this.count * 2
},
// define watcher (event listener), similar to Vuex watch
onChange(callback) {
// $watch is builtin method, used for watch changes
// $watch(source: Function, callback: Function): Function
unwatch = this.$watch(() => this.count, (newValue, oldValue) => {
console.log('onChange', newValue, oldValue)
unwatch()
})
}
// define an action, similar to Vuex action
increment() {
// $patch is builtin Vuex mutation, used for update state
// it accepts K:V object, will shallow asign to state
this.$patch({
count: this.count + 1
})
// for complex operation, can use a mutator function
this.$patch(state => {
state.count = this.count + 1
})
},
// also define an async action, similar to Vuex action
async query() {
let response = await http.get('/counter')
this.$patch({
count: response.count
})
}
})
We don't need define mutations, the builtin $patch mutation can handle all tasks.
import { counterStore } from '@/store/counter'
export default {
computed: {
// map state properties
count: () => counterStore.count,
// map getters
double: () => counterStore.double
},
methods: {
// map actions
increment: counterStore.increment,
},
async mounted() {
// call actions, or use properties
await counterStore.query()
console.log(counterStore.count)
},
destroyed() {
// the $reset method will reset to initial state
counterStore.$reset()
}
}
Congratulations, you can enjoy the hami melon flavored Vuex!
Hami-Vuex completely support TypeScript, you can configure TypeScript to make type inference smarter (better code intelligence).
// tsconfig.json
{
"compilerOptions": {
// this aligns with Vue's browser support
"target": "es5",
// this enables stricter inference for data properties on `this`
"strict": true,
}
}
Detailed instructions: TypeScript Support - Vue.js
If you don't need Vue SSR, you can skip the following advanced usages (which are not required in most cases).
In the above basic usage, the Store object is a stateful singleton, and using such an object in Vue SSR requires setting the runInNewContext parameter to true
, which incurs a relatively large server performance overhead.
Advanced usage avoids "stateful singletons" and is suitable for use in the Vue 3 setup method, as well as for Vue optional writing.
import { defineHamiStore } from 'hami-vuex'
// Note: Here is the capitalization of the initials CounterStore !
export const CounterStore = defineHamiStore({
/* Here the parameters are the same as hamiVuex.store() */
})
const vuexStore = /* Vuex Store instance */
const counterStore = CounterStore.use(vuexStore)
await counterStore.query()
console.log(counterStore.count)
export default {
setup() {
const counterStore = CounterStore.use()
await counterStore.query()
console.log(counterStore.count)
},
}
export default {
computed: {
// Similar to Vuex mapActions, mapGetters
counterStore: CounterStore.use,
count: CounterStore.count,
query: CounterStore.query,
},
}
const otherStore = defineHamiStore({
get counterStore(){
return CounterStore.use(this)
},
get count(){
return this.counterStore.count
}
})
Special thanks to the above projects and materials for bringing me a lot of inspiration, and hope that Hami Vuex can bring new inspiration to the community!
Stateful singletons are simpler and more convenient to use, and only perform poorly in terms of Vue SSR. Vue SSR is not required for most single-page applications, so you can use "stateful singleton" with confidence and switch to advanced usage if necessary.
Since TypeScript is difficult to type infer from the Vue optional interface, writing at the same level avoids this problem.
For specific reasons, please refer to the following information:
- microsoft/TypeScript#14141
- microsoft/TypeScript#13949
- microsoft/TypeScript#12846
- microsoft/TypeScript#47150
Because all fields need to avoid name conflicts after writing them at the same level, special fields are prefixed with $
.
The advantage of using mutations is that it is more convenient to debug in devtools, the disadvantage is that the code is slightly more cumbersome. Another problem is that after writing all fields at the same level, it is difficult to distinguish between actions and mutations. After weighing it, it was decided not to use mutations.
After using Pinia for a while, I felt that the devtools plugin support was not very good and unstable. In addition, Pinia is not written in a way that is friendly enough for Vue optional usage, and should be caused by supporting Vue SSR.
I think Vuex itself does a good job, and improving the interface design can achieve the effect I want, without having to make the wheels repeatedly. And based on the Vuex implementation, it can be compatible with old code to the greatest extent, and it is easy to do smooth migration.