Vue3 introduces the new feature Composition Api, which is a significant change from Vue2 code and provides a new approach to state management. Taking advantage of typescript’s full blessing in Vue3 allows us to use state management more gracefully.
Traditional state management – Vuex
The sample
Vuex is the official Vue production classic, support vue-DevTools debugging tool (currently 6.0.0 beta7 does not support, but will definitely support in the future), is still the first choice for global state management. Especially with the blessing of TS, state is silky. Here’s a simple example:
// store/modules/test.ts
import { Module } from 'vuex'
export interface User {
name: string
}
export interface TestState {
users: User[]
}
export default {
state: {
users: [{ name: 'sps'}},getters: {
userCount (state) {
return state.users.length
}
},
mutations: {
ADD_USER (state, user: User) {
state.users.push(user)
}
},
namespaced: true
} as Module<TestState, Object>
// store/index.ts
import { createStore } from 'vuex'
import test, { TestState } from './modules/test'
export interface State {
test: TestState
}
const store = createStore({
modules: {
test
}
})
export default store
// App.tsx
import { defineComponent } from 'vue'
import { useStore } from 'vuex'
import { State } from './store'
export default defineComponent({
name: 'App',
setup () {
const { state: { test }, getters, commit } = useStore<State>()
const addUser = () = > {
commit('test/ADD_USER', { name: 'haha'})}return () = > {
const items = test.users.map(user= > (
<li>{ user.name }</li>
))
return (
<div>
<div>{getters['test/userCount']}</div>
<ul>{ items }</ul>
<button onClick={ addUser} >add</button>
</div>)}}})Copy the code
Experience with
From the above code, we can see that TS is able to perform full override for state in the Vuex module, but it does not perform friendly type checking for mutation, action, and getter. Look at the type definitions in the Vuex source code:
// Vuex source type/index.d.ts
// In generics, S is the type of module state, and R is the type of root state
export interfaceModule<S, R> { namespaced? :boolean; state? : S | (() = >S); getters? : GetterTree<S, R>; actions? : ActionTree<S, R>; mutations? : MutationTree<S>; modules? : ModuleTree<R>; }export interface GetterTree<S, R> {
[key: string]: Getter<S, R>;
}
export interface ActionTree<S, R> {
[key: string]: Action<S, R>;
}
export interface MutationTree<S> {
[key: string]: Mutation<S>;
}
Copy the code
Commit (‘test/ADD_USER’, {name: commit(‘test/ADD_USER’), {name: commit(‘test/ADD_USER’), {name: commit(‘test/ADD_USER’, {name: ‘haha’}). Moreover, every function in the mutation, action, and getter is defined uniformly, and there is no way to type each function individually, so when you execute commit or Dispatch, you can’t type the parameters based on the name of the function being executed.
To take full advantage of ts type checking, you can use vuex-module-decorators.
Vuex module-decorators
The author is looking at vue-vben-Admin source code found that it has been used after this artifact. This artifact has been around since the Vue2 era (unfortunately before Vue and TS were combined), and the advantage is that the Vuex module is defined in a class way that takes full advantage of TS type checking.
The former is used to demonstrate
Two things to note before using vuex-module-decorators:
- In tsconfig.json, enable decorator syntax:
{
"compilerOptions": {
"experimentalDecorators": true
// ...}}Copy the code
- Create a utility function to solve the problem of dynamic module duplication when debugging hot updates:
// store/index.ts
export function hotModuleUnregisterModule (name: string) {
if(! name)return
if ((store.state as any)[name]) {
store.unregisterModule(name)
}
}
Copy the code
The sample
The dynamic module function of vuex-module-decorators can be used without manually registering modules in the store. Here is a simple example to use:
// store/modules/test.ts
import { getModule, Module, Mutation, VuexModule } from 'vuex-module-decorators'
import store, { hotModuleUnregisterModule } from '@/store'
const MODULE_NAME = 'test'
hotModuleUnregisterModule(MODULE_NAME)
export interface User {
name: string
}
@Module({ name: MODULE_NAME, dynamic: true.namespaced: true, store })
export default class UserModule extends VuexModule {
users: User[] = [{ name: 'sps' }]
get userCount () {
return this.users.length
}
@Mutation
ADD_USER (user: User) {
this.users.push(user)
}
}
export const testStore = getModule(UserModule)
// App.tsx
import { defineComponent } from 'vue'
import { testStore } from './store/modules/test'
export default defineComponent({
name: 'App',
setup () {
const { users, ADD_USER } = testStore
return () = > {
Getters are not responsive and need to be destructed in render
const { userCount } = testStore
const items = users.map(user= > (
<li>{ user.name }</li>
))
return (
<div>
<div>Total number of users: {userCount}</div>
<ul>{ items }</ul>
<button onClick={()= >{ADD_USER({name: 'haha'})}}> add</button>
</div>)}}})Copy the code
Experience with
Using vuex-module-decorators can take full advantage of TS features, but there is a small drawback. The original Vuex getters can take arguments, but the get attribute in class cannot pass arguments. However, there aren’t many applications for passing parameters in getters, and you can also write utility functions to solve this problem, so the experience is pretty good.
The new Vue3 feature implements state management
At VueConf 2021, VuUe.js core team member Anthony Fu talked about how using reactive objects in Vue3’s Composition Api can naturally implement state management without introducing any other libraries. On the basis of it, the related operation functions can be encapsulated together to use the hook function.
The sample
// store/modules/test.ts
import { computed, reactive } from 'vue'
export interface User {
name: string
}
export interface TestState {
users: User[]
}
Reactive (reactive); reactive (reactive); reactive (reactive)
const state: TestState = reactive({
users: [{ name: 'sps'}]})export default function useTestStore () {
const userCount = computed(() = > {
return state.users.length
})
const ADD_USER = (user: User) = > {
state.users.push(user)
}
return {
state,
userCount,
ADD_USER
}
}
// App.tsx
import { defineComponent } from 'vue'
import useTestStore from './store/modules/test'
export default defineComponent({
name: 'App',
setup () {
const { state, userCount, ADD_USER } = useTestStore()
return () = > {
const items = state.users.map(user= > (
<li>{ user.name }</li>
))
return (
<div>
<div>Total number of users: {userCount. Value}</div>
<ul>{ items }</ul>
<button onClick={()= >{ADD_USER({name: 'haha'})}}> add</button>
</div>)}}})Copy the code
Experience with
Make full use of the hook function, and use the Watch, onMounted, onUnmounted apis in the hook function to achieve richer functions. You can also enjoy the perfect blessing of TS without introducing other libraries, reducing the size of the packaged file.
But there are some drawbacks:
- – Vuex plugins are not available (Vuex has a mature community with many good plugins)
- Because of the loss of Vuex’s encapsulation, reactive objects can be manipulated directly from outside
Local state management
The above method can achieve global state management. In some scenarios, we need to maintain a local state in a component X and all its descendants. If multiple components X are used in the program, there will be multiple relatively independent local states, in which case provide/inject is needed.
The sample
Anthony Fu also mentions the use of type-safe provide/inject in VueConf 2021: That is to define the type of key used by provide/inject (when I see here, I suddenly think of the same way to achieve type-safe localStorage and Cookie access). Similarly, with a little simple encapsulation of its related operation functions, it can be used like the hook function.
// components/useTestState.ts
import { computed, inject, InjectionKey, reactive } from 'vue'
export interface User {
name: string
}
export interface TestState {
users: User[]
}
// Create local state
export const createTestState = () = > {
const state: TestState = reactive({
users: [{ name: 'sps'}]})return state
}
/ / define InjectionKey
export const injectTestKey: InjectionKey<TestState> = Symbol('Test')
export const useTestState = () = > {
// Make sure that the ancestor of the component that uses the hook function is provided
const state = inject(injectTestKey)!
const userCount = computed(() = > {
return state.users.length
})
const ADD_USER = (user: User) = > {
state.users.push(user)
}
return {
state,
userCount,
ADD_USER
}
}
// components/Test.tsx
import { defineComponent } from 'vue'
import { useTestState } from './useTestState'
export default defineComponent({
name: 'Test',
setup () {
const { state, userCount, ADD_USER } = useTestState()
return () = > {
const items = state.users.map(user= > (
<li>{ user.name }</li>
))
return (
<div>
<div>Total number of users: {userCount. Value}</div>
<ul>{ items }</ul>
<button onClick={()= >{ADD_USER({name: 'haha'})}}> add</button>
</div>)}}})// App.tsx
import { defineComponent, provide } from 'vue'
import Test from '@/components/Test'
import { createTestState, injectTestKey } from '@/components/useTestState'
export default defineComponent({
name: 'App',
setup () {
const state = createTestState()
provide(injectTestKey, state)
return () = > {
return (
<Test />)}}})Copy the code
Experience with
The provide/ Inject implementation’s local state management experience is also consistent with the development style of the Vue3 Composition Api. The biggest drawback is still the issue of encapsulation. Reactive objects can be manipulated directly by components. If you need to ensure that the reactive object is not directly operated by the component, you can either provide the subcomponent with a get ComputedRef containing the value of the reactive object, or provide a function, instead of providing the reactive object directly to the subcomponent. It returns the cloned value of the reactive object.
conclusion
This article describes several state management methods have their own characteristics, according to the actual scenario to choose which one to use. Due to the naturally shared nature of the Vue3 Composition Api, Vuex is no longer almost the only option for state management. Many of the problems that need to be solved with Bus and mixins in Vue2 can be solved with the sharing of composition Api.