Vue3 has been around for quite some time, and I recently made full use of vue3 features in a company project. Compared with VUe2, the whole coding method of VUe3 has not a small change, if you want to write simple and elegant code, it may still need a certain amount of time to explore. In the process of exploration, one of the questions I need to face is: how can I better manage the state of VUE3?

In the era of VUe2, a ready-made state management tool, Vuex, borrowed from redux of the React ecosystem, defined a state variable, and then defined its getter, setter, and asynchronous change action methods. Overall, this solution is perfectly fine for day-to-day business development — it’s just a little cumbersome to write.

However, now that we have entered the vuE3 era, the disadvantages of Vuex are more obvious. First and foremost: Doesn’t fit well with typescript. Vue3 has been rewritten with TS, giving full play to the role of TS type system; Vuex: The current design of vuex is that when I use the state change method, I pass in string names. ! Take a look at an example from the official documentation:

actions: { async actionA ({ commit }) { commit('gotData', await getData()) }, async actionB ({ dispatch, Commit}) {await dispatch('actionA') // await actionA to complete commit('gotOtherData', await getOtherData())}}Copy the code

In this way, it is completely impossible to achieve intelligent type hints, which is really unacceptable for me who rely heavily on TS.

Vuex also has a second drawback: its verbose syntax and separation from vue3’s functional STYLE API — not as much of a problem as the first one, but still a pain in the neck. In short, for me, these two drawbacks are basically the death sentence for vuEX.

Therefore, I began to explore a more reasonable state management scheme in the actual project. I turned my attention to vue3’s own set of apis: provide/ Inject.

The documentation explains that the provide function is called in the parent component, passing in the string token as the first argument and the object that the child component needs to access as the second argument. Then call inject function in the subcomponent, pass in the same token, will get the object value. The code is as follows:

// Parent.vue
import { provide } from 'vue';

provide('person', {name: 'bob', age: 20});


//Child.vue
import { inject } from 'vue';
const person = inject('person');
Copy the code

This is the simplest example of how to pass values in parent components via provide/inect. At first glance, it doesn’t seem to be much use, because it’s just an ordinary object that doesn’t have the ability to update in a responsive manner. However, remember that we are using VUE3, and to be responsive we need to pass ref/ Reactive objects. So I changed the code slightly:

(Parent component)

// Parent.vue
<script lang="ts" setup>
  import { provide, reactive } from "vue";

  const person = reactive({name: 'bob', age:32});
  provide('person', person);
 </script>

<template>
  <div>
    <child></child>
  </div>
</template>
Copy the code

(Subcomponent)

// Child.vue <script lang="ts" setup> import { inject, onMounted } from 'vue'; const person = inject('person'); onMounted(() => { person.age = 25; </script> <template> <div> MY name is {{person.name}} my age: {{person.age}} </div> </template>Copy the code

In the parent component, a reactive object is provided through provide; This object is then injected into the child component. Modify the age property of the object in the child component, and the view updates in response! Similarly, if the Child component has its own child component, call Inject is also valid. I won’t say much more about this, after all, these two ancient apis have been around since the time of VUe2, but with VUE3, they are finally not useless, but useful!

Now the problem seems to have been solved. With provide/inject and ref/ Reactive, the problem of parent/sibling components sharing state is solved. But as the business deepened, I fell into a meditation, this program still has serious problems, need to rescue. It is mainly reflected in two points:

First, our provide method is still passing a string.

Provide /inject The current design is that there is a string (or symbol) between them to establish the cipher, when the cipher matches, I will give you the corresponding value. But if the business is complicated, many codes, one carelessly one of the spelling mistakes, that is not to find the eye? Also, this string pass-through method still doesn’t have a good type hint. Take a look at the code I wrote in the vscode editor:

The age attribute does not exist on type “unkown”. Why? Because inject function itself needs to be passed generic, if not, the system will consider the object type bit unkown returned.

Every time I call inject, I have to manually write a type declaration. Something like this:

const person = inject<{name: string; age: string}>('person');
Copy the code

Sorry, that’s not how typescript works.

Second: passing a reactive object directly in provide lacks encapsulation and logical reuse capability.

Now looking back at VuEX, although wordy and type-hinting unfriendly, it provides a complete state management solution that allows components not only to share state but also to reuse a series of logical methods for changing state. That’s what the mutation Action did.

Look at what we did earlier: pass reactive objects directly down, and the logic for state changes is still written in the component, which is not scientific. Do we have to create a larger object in a separate file that includes state and state change logic? Such as:

//person.ts const personStore = { state: reactive({ name: bob, age: 20 }), setAge(n: number) { this.state.name = n; }}Copy the code

It looks a bit like Vuex and uses this, which Vue3 was trying to get rid of. It’s extremely inelegant in any way. It doesn’t even have the class object-oriented initialization and encapsulation capabilities. After all, some methods might just be called internally and not want all of them exposed. And in the process of coding, I found that the generic hook functions I had encapsulated could not be easily used in such state objects. In a word, within the framework of VUE3, it is not acclimatized.

With all these questions, what should we do? Just compromise, finish the business function early and leave?

Hey.. Wait, let’s go back to the beginning and get back to the original question — why did yu creek release vue3 with such a big change? Why do we have to switch from VUe2 to VUe3 again?

The official explanation is that using the Composition API allows for greater logic reuse. This means that much of the business logic that used to be written in components can now be easily abstracted out and placed in a single function. The official document also gives an example:

// src/composables/useRepositoryNameSearch.js

import { ref, computed } from 'vue'

export default function useRepositoryNameSearch(repositories) {
  const searchQuery = ref('')
  const repositoriesMatchingSearchQuery = computed(() => {
    return repositories.value.filter(repository => {
      return repository.name.includes(searchQuery.value)
    })
  })

  return {
    searchQuery,
    repositoriesMatchingSearchQuery
  }
}
Copy the code

Here is an example of abstracting a search function into a composite function. Of course, I prefer to call it a hook function with React. Inside a hook function, we can use a component lifecycle hook function such as onMounted. At the same time, reactive variables defined in the hook can be returned to the component to access. If a change is made inside the hook, the component view is updated accordingly.

This is vuE3’s great capability for logical reuse. But what does this have to do with state management?

It’s too much.

By now, the conclusion is obvious. And I think this is probably an official omission of VUe3. It only advertises the logic reuse capability of hook functions (officially called combinative functions), but does not mention that this scheme is also very natural for state management. Probably because vuex/ Redux and the like are so influential to developers, when it comes to state management, they’re the starting point, and you end up with a variant of Redux. In fact, with hooks, the problem of state management is destined to be involved. Now, it’s time to get rid of the baggage of vuex and other functional styles that don’t go with them.

Think about it. Every time a component uses a hook function, the function is called once, creating a whole new execution context and closure. If hook function F returns A reactive variable x, then components A and B use hook function F respectively, and they only get their own variable X. However, what about x in some business scenarios where I want A and B to share the same variable?

Provide /inject the CP pair is back on the scene.

Consider the following business scenario: I wrote a hook function full of user-specific logic, such as user information modification and login and logout. In general, user-specific state and logic may be available to all components. Obviously, it should be globally unique. If each component uses this hook function separately, it cannot share user-specific state variables.

export function useUserInfo() { const userInfo = reactive({ }); const login = (data) => {... }; const logout = () => {... }; const editUserInfo => (data) => {}; return { userInfo, login, logout, editUserInfo } }Copy the code

What if you pass in the userHook function by calling provide directly from the root component?

//app.vue

<script>
import {provide} from 'vue';
import {useUserInfo} from '@/hooks/userUserInfo';

provide('user', useUserInfo())
</script>
Copy the code

Congratulations on discovering hua Dian! Now everything becomes clear: after providing, you can use inject access to the object returned by the useUserInfo function in the Login component and call the login method of this object. The logOut method returned by this object is called in the logOut component, and user information is rendered in the userInfo component using the returned userInfo variable. Everything is so natural that as soon as the logic of state change is executed in any one place, all related components can respond to the update.

Outside the view, it’s all hooks.

Vue3 era, this is my programming philosophy. The component is only responsible for accessing the reactive variables returned by the hook and throwing them to the template. The rest of the business logic, state management, is all up to the hook. This is the complete separation of view and logic from state. So, with React Hook and VUE3, the front-end era has really changed. (Of course, as a big angular fan, I thought Angular got here three years ago, too early to be a pioneer.)

Of course, after all this talk, the final solution for state management is available, which is ** regression hook. ** If you want the state and logic inside the hook to be shared among multiple components, you only need to add a provide/inject on the basis of the hook; If you want the hook function to generate a new state in each component, just use it as usual in the component. When you think about it, our solution doesn’t seem to be doing anything new. It’s much simpler than vuex. Therefore, after the emergence of Hook, the problem of state management has been fundamentally resolved.

But wait… I don’t think the TS problem has been solved yet. Actually solve this problem only need provide/inject a layer of encapsulation. Without further ado, let’s go straight to the code I wrote in my project practice:

// Export interface FunctionalStore<T extends Object > {(... args: any[]): T; token? : symbol; root? : T; UseProvider returns the result of the call to the hook function by default, since the inject function only looks up from the parent component. Export function useProvider<T extends Object >(func: FunctionalStore<T>): T {! func.token && (func.token = Symbol('functional store')); const depends = func(); provide(func.token, depends); return depends; } // Export function useProviders(... funcs: FunctionalStore<any>[]) { funcs.forEach( func => { ! func.token && (func.token = Symbol('functional store')); provide(func.token, func()); }); } / / encapsulation type of primary inject InjectType = 'root' | 'optional'; // Accept the second argument, 'root' to indicate direct global use; Optional Hook export function useInjector<T extends Object >(func: FunctionalStore<T>, type? : InjectType) { const token = func.token; const root = func.root; switch(type) { case 'optional': return inject<T>(token) || func.root || null; case 'root': if(! func.root) func.root = func(); return func.root; default: if(inject(token)) { return inject<T>(token) }; if(root) return func.root; Throw new Error(' state hook function ${func.name} is not provided in the upper component by calling useProvider '); }}Copy the code

The above is my state management solution based on vue3 provide/ Inject. There are only two basic apis, useProvider and useInjector, with complete hook function capability and complete automatic type prompt. Note that both functions pass in the hook function itself and not the string token. Token is directly attached to the properties of the function, eliminating the need to manually fill in the token during encoding.

The following is my actual use case in the company’s project:

The app.vue root component passes in five hook functions at once

A simple implementation of one of the hook functions (useState is a wrapper around vue ref, to look like react…)

When you inject the corresponding hook function into the child component, you can see the editor type prompt (no need to declare the type manually at all).

Because the business was not complex, it did not seem to capture the power of the scheme. Of course, scalability is also one of its advantages, the project is not complex and easy to use. With the expansion of business, I think this scheme can hold the whole project. Now we can imagine an application scenario to see what Hook +provide/inject can do.

In vue3 development, many people will write a common Ajax request hook function based on the composite API. Some common logic for the request is put in this code, like the following:

export function useRequest<R>(url: string, option: any) { const res = ref<R>(null); const status = ref('pending'); const checkHttpStatus = (status: OnMounted (() => {fetch(url, option).then( response => { if(response.ok) { res.value = response.body; status.value = 'success' } else { checkHttpStatus(response.status); status.value = 'failed' } }); }) return { res, status } }Copy the code

The above is a simple generic request function that automatically sends a request when used within a component or other hook. But in many cases, that bit of code won’t hold the entire business. For example, permission authentication for request interception is common.

So what do you do, just add permission verification code to useRequest? That’s too much coupling. Write it in a function and call it, okay? Well, yes. However, if we take a higher perspective, the useRequest hook is intended to be used by multiple projects in the company’s common library, so it is not appropriate to call the permission validation function directly. Each project’s business logic is different.

Provide /inect again!

First, we specify a token to indicate the request blocking function required by our useRequest as follows:

export const  HTTP_INTERCEPT = Symbol('intercept');
Copy the code

Now, if one of your colleagues is using the useRequest method you wrote and wants to useRequest blocking. So, he needs to write an interceptor function first, and then introduce the above secret HTTP_INTERCEPT to the token attribute of the function (please refer to one of the functionalStore interfaces I defined above!).

//httpIntercept.ts export function httpIntercept() { const auth = useAuth(); Const intercept = () => {/** useAuth: return a Promise */ return new Promise(resolve => {... }) } return intercept } httpIntercept.token = HTTP_INTERCEPTCopy the code

We then provide the interceptor in app.vue via useProvider:

//app.vue
<script>
  useProvider(httpIntercept);
</script>
Copy the code

Ok, so now our useRequest needs a makeover

export function useRequest<R>(url: string, option: any) { const res = ref<R>(null); const status = ref('pending'); // Note the second parameter optional, which indicates that the interceptor is optional. By default, null const Intercept = useInjector(HTTP_INTERCEPT, 'optional'); const checkHttpStatus = (status: */} const request = () => {fetch(url, option).then( response => { if(response.ok) { res.value = response.body; status.value = 'success' } else { checkHttpStatus(response.status); status.value = 'failed' } }); } onMounted(() => { if(! intercept) { request() } else { intercept().then( _ => { request() }) } }) return { res, status } }Copy the code

As you can see, the provide/inect hook functions combine code reuse and decoupling to the extreme! State management is just a convenience that comes along the way.