Vue Composition API

Vue Composition API RFC

This name is too long, it is referred to as 3.0API or hook.

3.0 has very big changes in the API level, most of which are developer-friendly, but some of which seem to be a bit more bullshit, so I’ll write an article about it later.

Here is a list of some of the core changes I think, after reading should be able to have a macro understanding.

Abandoned the Options API

Data, methods, computed, mounted, and so on will not be available in the 3.0API, but I will not go into details about the changes in the RFC. Instead, there is a setup function.

The name setup has been sprayed around the community, but is actually a more accurate description when you think about it.

This function can be used in two ways:

  • Returns aObject, values can be accessed in the template
  • Returns a function whose return value is a Render function/JSX

This function is used to mount data, so it can be understood as a create procedure, replacing the beforeCreate and Created hooks.

Independent responsive system

Responsivity 2.0 is strongly bound to the Vue instance so that data on the Vue instance will have responsivity. In 3.0, however, responsiveness exists as a separate system and does not necessarily work with Vue.

The core purpose of this design is decoupling, and it is only after decoupling reactive and Vue that the old API can be discarded. State and setState are strongly coupled, making it difficult to extract logic completely. Use useState to remove this coupling.

After reactive decoupling, you just need to separate out the corresponding logic and return the required states and methods in setup. Analysis and understanding of 3.0 design will be a separate article. In practice, development flexibility has improved a lot, but going from 2.0 to 3.0 required adapting to a big change in thinking.

No Class API

In vue-Next code, there is no class, everything is implemented by functions and closures. The purpose is also clearly mentioned in Motivation:

Better Type Interface

As for why the class Based API was abandoned, it is discussed in detail in the RFC and I won’t leave you any more details. In practice, type derivation is not too cool.

Why do we do this

The form in our business scenario has the following characteristics:

  • Step by step, step between pages
  • Form item linkage has a wide range
  • The form flow varies from scene to scene, but not much, such as a page that is 80 percent the same, causing reuse problems
  • Form item functionality can be reused in many scenarios

Therefore, we need an engineering practice to solve the problems of reuse and form linkage. This practice takes advantage of the features of the 3.0API to improve the development experience and efficiency.

One common idea is to use the global model and context

Of the current scenarios, I think the best is to use a global Model object to store form item data. A more detailed one will differentiate between model and context to better divide the logic.

The nice thing about this solution is that it’s flexible, and the bus mechanism lets you do whatever you want at the form item level, no matter how dynamic, as long as the data is eventually concatenated by the bus.

At the design level, the Model and context can provide a complete context for a form item, where you can focus on the business logic without caring about other information outside the form item’s domain.

But it also has the disadvantage of being flexible. The model and context are designed so that their values are determined at run time. If you want the development phase to get the type information for the model and context, you need to manually define the interface and declare the type for the Model and context in a very ugly way.

This problem is difficult to solve at the design level because of its runtime nature.

So said

There are several problems with this approach:

  • Loss of type inference at development time

    This is mostly a matter of pleasure

  • Model and context injection methods

    There are two types of injection, prop passing and global variable reading.

    With Prop, you run into the problem of transparent multilayer components.

    Using global variables is itself a strongly coupled operation. But it’s a matter of opinion whether or not you really want the form component to have that much reusability, and generally that component is used within this set of forms.

  • The stripping of form items and environments is incomplete

    The ideal abstraction for a form item is a component that provides a set of data. What the key of this set of data is in the final result should not be sensed in the form item. However, when using the Model scheme to deal with form item association, if you treat the key as an unknown quantity, you will run into problems because the key to be associated is unknown. There are two ways to do this: the dependent item writes the context when the value changes or uses a fixed innerKey that is used inside the form item.

    The first method, because context is run-time and has no type, needs to be negotiated between different components and has high maintenance costs.

    The second method, it actually creates coupling, and it’s unknown whether the value of each innerKey exists, what the data type is, right

    Therefore, the biggest problem of model scheme is incomplete decoupling.

Of course, this program is not bad, the deficiencies of this program is mainly in development, from the business to talk about technology are playing rogue. This approach works well for products such as visual drag-and-drop generated pages, where the key of the form item and the relationship between the form are relatively determined and no development is required.

The core idea

  • A form consists of a set of form items
  • A form item is a relatively independent field whose function is to provide a set of data
  • Calling the encapsulated form item component from the Vue form component is actually a configuration. The configuration description language is JS and Vue Template
  • Form items do not depend directly on other form items, and the coupling of multiple form items is implemented at the form level

The data series

Each form item component provides a getData function that returns the data exposed by the form item.

The form provides the getFormData function to complete the combination of form item data and specify the key of each data for interaction with the back end.

Since the getData type is derivable, the getFormData type is also derivable. Now you need to consider where getData takes the data from.

Component data storage and reuse

GetData of a form item component should not be exposed through Vue real exceptions. In fact, in 3.0 apis we should try to avoid exposing methods through Vue instances. One of the purposes of hooks is to extract methods from a class.

The problem with this design is that states cannot be stored in the Vue instance, only externally.

The problem with using singletons outside is reuse. You can’t simply put them in variables, or you’ll get the same reference when a component is called multiple times.

So we need to design a map structure to store multiple values. Components can locate states of form items based on their own keys. Let’s call this map cache.

We can design a function to construct the cache and getter, like this

const [cache, getCacheByKey] = getChache({value: 'foo'}) // The argument is states
Copy the code

The advantage of this is that the cache is built in a uniform manner and the complete type information of the States can be retained.

GetData can then be derived from successfully retrieving the data and retrieving the data type.

example

The final implementation looks like this

export const [
    inputCache,
    getInputStates,
] = cacheFactory({ value_: ' ' })

export const getInputData = (key) = > {
    return inputCache[key].value_
}
Copy the code

These variables need to be exposed in a separate TS file because they are defined in the shim-vue declaration file. The Vue file exports a component object, there is no other method, so writing in the Vue file is not recognized by TS.

This sort of defeats the purpose of code orgization, and exporting directly from vue actually works, and should be solved by writing a plug-in.

cacheFactory

So here’s my implementation

Note that this article is just a way of thinking about things like cacheFactory naming, implementation, etc. It’s not necessary, it’s just convenient for me.

type Cache<T> = {[key: string]: UnwrapRef<T>}

export function cacheFactory<T> (data: T) :Cache<T>,key? :string) = >UnwrapRef<T>] {
    let cache = {} as Cache<T>
    const getStates = (key = 'default') = > {
        if (cache[key]) {
            return cache[key]
        }
        cache[key] = reactive(deepClone(data))
        return cache[key]
    }
    return [cache, getStates]
}
Copy the code

Note that in our case, the cacheFactory actually receives the format or type of the States, so the cache must have been deep-copied or otherwise converted. Deep copy limits the type of data here, but I think the data here is the source of the form items that are exposed to the outside world, so it’s not a big problem to use deep copy.

Another pitfall comes from Vue’s type declaration. Going back to the example above, I used value_ instead of value as the key. The problem comes from the UnwrapRef declaration.

declare type BailTypes = Function | Map<any.any> | Set<any> | WeakMap<any.any> | WeakSet<any>;
export interface Ref<T> {
    value: T;
}
export declare type UnwrapRef<T> = T extends Ref<infer V> ? UnwrapRef2<V> : T extends BailTypes ? T : T extends object ? {
    [K in keyof T]: UnwrapRef2<T[K]>;
} : T;
declare type UnwrapRef2<T> = T extends Ref<infer V> ? UnwrapRef3<V> : T extends BailTypes ? T : T extends object ? {
    [K inkeyof T]: UnwrapRef3<T[K]>; } : T; .// In the middle are many layers of UnwrapRef definitions
declare type UnwrapRef10<T> = T extends Ref<infer V> ? V : T;

Copy the code

If the value field is used, matchT extends Ref

is parsed into T in Ref. So if {value: “} is used, the returned cache will be parsed to string.

Context

Context is where we focus on expecting type derivation, so that we can know exactly what the context is between components at different steps, rather than relying on development-time conventions.

Use Vuex or Context objects

I have used two schemes:

  • Store context data based on Vuex
  • Set a context object at the form level to hold data

For the use of Vuex, it is actually a better solution under the framework of 2.0, which can be annotated in some ways. However, it should be used in the code with strong business and weak reuse, and should not be coupled with common reusable components as far as possible.

The context object is implemented in the model/ Context schema above. In this pattern, because the context is the external environment information provided to the form item, it must be coupled to the form item logic. The problem with this is that the external environment for a form item is variable, so it is not known if a field in the context exists. This creates a lot of uncertainty.

Of course, this disadvantage is based on the ideal assumption that the internal and external environments of form items are completely decoupled. If it’s not designed with that in mind, there are solutions.

Static context

If we want the context’s type to be derivable, we have to design it to be static, we can’t do this stuff at runtime. This used to be difficult because reactive data was bound to Vue instances.

A better approach is to use a separate Vue instance as a data container, where static functions store data. This is essentially a simplified version of Vuex, where the underlying Vuex is also responsive using Vue instances. Now responsive system independence makes this much easier.

Some lessons can be learned from the above two schemes:

  • Using a context in a form item creates coupling
  • It makes sense to distinguish between the internal and external environments of form items, and then receive the required data through parameters

Relying on the design described in the previous section to expose form item data through an external Cache, we can implement a Context with full type inference.

example

Here is a direct example from my development practice. You can look at the summary of the main points in the next section before looking at the project code.

// context.ts
export function getLandingContext () {
    // Get the values in the form page
    let downloadType = computed((a)= > getCheckBoxStates(formKeys.downloadType).value_)
    let platform = computed((a)= > getCheckBoxStates(formKeys.platform).value_)
    let urlInput = getUrlInputStates()
    // Attributes that are multi-dependent
    const isApp = computed((a)= > downloadType.value === LandingType.DOWNLOAD_URL)
    const isIOS = computed((a)= > platform.value === Platform.iOS)
    return {
        isApp,
        isIOS,
        // The form item depends on the upper and lower parameters
        platformOpts: computed((a)= >
            isApp.value
                ? landingFields.platform_app
                : landingFields.platform_link
        ),
        convertFetchType: computed((a)= >! isApp.value ?'external'
                : platform.value === Platform.iOS
                    ? 'ios' : 'android'
        ),
        urlInputType: computed((a)= >
            isApp ? 'app' : 'link'
        ),
        convertDisabled: computed((a)= >
            urlInput.url === ' '|| urlInput.inputing || !! urlInput.error ), convertHint: computed((a)= > {
            if (urlInput.inputing) {
                return 'Please finish typing first'
            }
            return! urlInput.url ?'Please fill in the download link above correctly first' : ' '
        }),
        // Context-dependent calculation functions
        needAlertOnChange: (a)= >
            localStorage.getItem('landingChangeAlert')! = ='1'&& (!!!!! convert.url || ! isEmpty(convert.convert) || !! webUrl.url) , } }// globalContext.ts
export const generalContext = (a)= > ({
    ...getLandingContext(),
})

// useage
setup () {
    const context = generalContext()
    // ...
}
Copy the code

points

To summarize some key points, you can find references in the code above.

  • Context structure and reference
    • Form page/module context function
    • Merge all the functions of the context (let’s call it mixedinContext)
    • The component gets the global context object from the mixedinContext
  • Table item values in the function need to be maintained using computed to be responsive, which is something to be aware of in 3.0API development.
  • The computed properties in a form are almost always context-dependent, so most of them are defined directly in the context function. The advantage of this is that you don’t have to worry too much about the context content in the form component and do some logical encapsulation.
  • Context normally returns:
    • Computed, responsive
    • A function that returns some value will always be called in the associated logic, so the loss of responsiveness does not affect functionality
  • The context function content can be implemented in any way, but care is taken to maintain its responsive nature.
  • Context is strongly associated with the content of a form. Different forms need to define different context functions, and repeated context calculation logic can be extracted

Reuse logic: global context <- module context <- single context

State of merger

Let’s consider a case where several items in a form have very strong relationships, such as the following business scenario.

The target list needs to be selected based on the link address or application package name. The application package name is also pulled from the link address entered. After re-entering or selecting the link address or application package name, you need to clear the selected conversion target and pull it again. There’s some other form logic that I won’t go over here, but you know it’s complicated.


Using the encapsulation logic described above is not appropriate because the purpose of separating form items is to encapsulate them effectively rather than increase the cost of form linkage.

At this point you might be thinking, why not merge everything in the diagram into one component? But doing so leads to a significant reduction in component reusability. This is because it couples the form’s framework with the form item’s content. The frame is the label of the form item, the obligatory dots in the middle and the layout, and the content is the input box on the right, the button, and so on. When we wrap the form item component, we implement the content on the right side, whatever the content on the left side is.

The way we implement this is also relatively simple: we extract the states of these associated options into a large state that the form eventually reads. This does not affect the reusability of single form items. Form items can still maintain their own cache by using the watch function to update the external merged state at the right time. For a large number of reusable form items, watch can be implemented at the form level, and the form items that will not be reused can be directly associated with external state.

The advantage of this approach is that the data linkage logic and external state between form items are encapsulated in a file, which can be implemented through watch and other methods, and even expose some hook execution operations to the form. These associations are no longer managed at the form level. This is one of the design goals of the 3.0API.

Form combination

Configurational thinking

Componentization and configuration are always a good way to solve dynamic forms problems once and for all. But this issue really needs to be looked at differently by business scenarios and objects. For example, a drag-and-drop form that needs to be used by operations students should be described using JSON and rendered by the front-end Runtime. However, some forms with complex business logic that need front-end development are not suitable for this configuration idea, because the transformation from JS to JSON is a dimension reduction of language expression ability.

One of the points mentioned here is that configuration is really a trade-off between configuration language expressiveness and business logic complexity. The lower the configuration language expressiveness, the more difficult and complex it is to express complex business logic. Json is a language with weak expression ability, so using JSON to generate forms must be geared to the case of mediocre business logic complexity.

What if we describe the configuration directly in JS? It’s certainly doable and highly efficient. For example, the button group here is different for each step, and the click callback is also different. Its arrangement, style, callback and other characteristics are described through an object.

Furthermore, since the project is being built using Vue, it is certainly possible to describe the configuration using Vue syntax. This is what I mentioned in the Core Thinking section. If the component encapsulation is high enough, the Vue component itself is a configuration description of the form. That’s why you put a lot of calculated properties into the context function, and why you extract the form linkage logic. This design leaves the form component itself with very little logic, just associating the component with the logic.

We composed the form according to the following points

The form item component is nested within the form frame component

<moduleItem title="Download mode" required>
    <FormCheckBox
        :form-key="formKeys.downloadType"
        :fields="formFields.downloadType"
        :disabled="reviewing"
        :filter="changeFilter"
        :onChange="downloadTypeChange"
    />
</moduleItem>
Copy the code

The definition of a form framework was discussed in the previous section.

Use Context to control form item behavior

Such as whether to display, data source, display content, etc

If there are multiple levels of if nested logic between form items, we level it down to one level, which is a computed value anyway.

Defined for each form moduleinitandgetDatamethods

The form module has its own logic for initializing data and methods for outputting data based on the context. These methods are unique to the form module and will not be reused. The content of the form submission is retrieved from the getData method.

As for where the Ajax request logic goes, it doesn’t matter if the form is there, it just needs to be there to get the data, anywhere.

Conclusion

The above describes our engineering practices for the Vue Composition API in form development. The reason for writing this article is that with the help of the 3.0API, form scenarios can be componentized and encapsulated in a more flexible way.

The business scenario for forms is very variable, and this article is just one way to think about it. Some of the designs may be flawed. We expect to achieve nothing more than:

  • Better componentization and reuse
  • Better logical segmentation
  • Better type derivation

Hopefully, this article provided you with some inspiration to understand the Vue3.0API.