preface

High-order components are a concept that has been popular in React, but not much discussed in the Vue community. This article will take you through some of the more advanced operations.

Just to be clear, the core of this article is to learn the idea of decoupling an intelligent component from a puppet component. It doesn’t matter if you haven’t heard of the concept, it will be explained below.

This can be done in many ways, such as slot-scopes, and in the future, composition-API. The code written in this article is not recommended for use in the production environment. There are more mature libraries to use in the production environment. This article focuses on the idea of adapting the React community gameplay.

Don’t spray me, don’t spray me, don’t spray me!! This article is just to demonstrate ideas for higher-order components, but if you want to simplify the asynchronous state management mentioned in this article in real business, use the slot-scopes based open source library vue-Promised

In addition, the mention of 20K in the title is actually a bit titleparty. What I want to express more is that we should have such a spirit. Knowing only this skill will certainly not make you reach 20K. But I believe that with the dedication to advanced usage, business code, and efficiency, we will get there, and that day is not far away.

example

This article uses the most common requirements in daily development, namely the request for asynchronous data as an example.

<template>
    <div v-if="error">failed to load</div>
    <div v-else-if="loading">loading...</div>
    <div v-else>hello {{result.name}}!</div>
</template>

<script>
export default {
  data() {
    return {
        result: {
          name: ' ',},loading: false.error: false,}},async created() {
      try {
        Loading / / management
        this.loading = true
        / / get data
        const data = await this.$axios('/api/user')  
        this.data = data
      } catch (e) {
        / / the error management
        this.error = true  
      } finally {
        Loading / / management
        this.loading = false}}},</script>
Copy the code

We usually write in this way, and there is no problem with it. But in fact, every time we write asynchronous requests, we have to have loading and error states, and we need to have logic to fetch data, and we need to manage these states.

So how about abstracting it? The React community used HOC (high Order Component) to handle such abstractions before hooks became popular.

What are the higher-order components?

At this point, we need to think about what a higher-order component is. In the end, a higher-order component is:

A function takes a component as an argument and returns a wrapped component.

In the React

In React, components are classes, so higher-order components are sometimes implemented using decorator syntax, because the essence of decorators is to accept a Class and return a new Class.

In the React world, higher-order components are f(Class) -> the new Class.

In the Vue

In the world of Vue, a component is an object, so a higher-order component is a function that takes an object and returns a new wrapped object.

By analogy to the world of Vue, higher-order components are f(Object) -> new Object.

Smart components and puppet components

If you’re not familiar with the concept of puppet components and smart components, I’ll give you a quick rundown. It’s a well-established concept in the React community.

Puppet component: Like a marionette, render the view based on the props passed in from outside, regardless of where the data came from.

Intelligent component: generally package in the puppet component outside, through requests and other ways to obtain data, passed to the puppet component, control its rendering.

In general, their structural relationship looks like this:

<Intelligent components>
  <Puppet components />
</Intelligent components>
Copy the code

They also have another nickname, container component and UI component.

implementation

In the example above (go back and read it if you forgot, haha), the idea is this:

  1. Higher-order component acceptancePuppet componentsRequested methodAs a parameter
  2. inmountedData is requested during the life cycle
  3. Pass the requested data throughpropsPassed to thePuppet components.

First of all, as mentioned above, HOC is a function. This time, our requirement is HOC to realize request management. So we define it to accept two parameters first, and we call this HOC withPromise.

Loading and error states, as well as views of loading and loading errors, should be defined in the new wrapper component returned, that is, the new object returned in the function below.

const withPromise = (wrapped, promiseFn) = > {
  return {
    name: "with-promise",
    data() {
      return {
        loading: false.error: false.result: null}; },async mounted() {
      this.loading = true;
      const result = await promiseFn().finally((a)= > {
        this.loading = false;
      });
      this.result = result; }}; };Copy the code

In the parameters:

  1. wrappedThat is, the component object that needs to be wrapped.
  2. promiseFuncThat is, the corresponding function to the request needs to return a Promise

This looks good, but we can’t write templates in the same way we write templates in the.vue single file.

But we know that the template will eventually be compiled into the render function on the component object, so we’ll just write the render function. (Note that this example uses the original syntax for demonstration purposes; scaffolding projects can be created using JSX syntax directly.)

In the render function, we wrap the wrapped puppet component passed in.

The intelligent component gets data -> the puppet component consumes data, and the data flows.

const withPromise = (wrapped, promiseFn) = > {
  return {
    data() { ... },
    async mounted() { ... },
    render(h) {
      return h(wrapped, {
        props: {
          result: this.result,
          loading: this.loading, }, }); }}; };Copy the code

At this point, with a barely usable prototype, let’s declare the puppet component.

This is a separation of logic and view.

const view = {
  template: `  {{result? .name}}  `.props: ["result"."loading"]};Copy the code

Note that the component here could be any.vue file, but I’m just using it for simplicity.

And then something magical happens, don’t blink, we wrap this view component withPromise.

// Pretend this is an AXIos request function
const request = (a)= > {
  return new Promise((resolve) = > {
    setTimeout((a)= > {
      resolve({ name: "ssh" });
    }, 1000);
  });
};

const hoc = withPromise(view, request)
Copy the code

Then render it in the parent component:

<div id="app">
  <hoc />
</div>

<script>
 const hoc = withPromise(view, request)

 new Vue({
    el: 'app',
    components: {
      hoc
    }
 })
</script>
Copy the code

At this point, after a blank second, the component renders my name SSH, and the entire asynchronous data stream runs through.

Now add loading and loading failure views to make the interaction a little more user-friendly.

const withPromise = (wrapped, promiseFn) = > {
  return {
    data() { ... },
    async mounted() { ... },
    render(h) {
      const args = {
        props: {
          result: this.result,
          loading: this.loading,
        },
      };

      const wrapper = h("div", [
        h(wrapped, args),
        this.loading ? h("span"["Loading...") :null.this.error ? h("span"["Loading error") :null,]);returnwrapper; }}; };Copy the code

The code up to this point can be viewed in the preview, and the source code can be viewed directly in the console source.

perfect

The higher-order component so far is demonstrable, but not complete, and lacks some features such as

  1. To get the parameters defined on the child component as parameters to initialize the send request.
  2. Listen for changes in the request parameters in the child component and resend the request.
  3. The external component passes tohocComponent parameters are not passed through now.

The first point is easy to understand; the parameters of the scenario we requested are flexible.

The second requirement is also a common requirement in real world scenarios.

For example, when we use hoc in the outermost layer, we might want to pass some extra props or attrs or even slots to the innermost puppet component. As a bridge, hoc components should assume the responsibility of transmitting it through.

To implement the first, we agreed that the View component would need to mount a field with a particular key as the request parameter, for example, we agreed that it would be called requestParams.

const view = {
  template: `  {{result? .name}}  `,
  data() {
    // Take it with you when sending requests
    requestParams: {
      name: 'ssh'}},props: ["result"."loading"]};Copy the code

So let’s rewrite our request function so that it’s ready to accept arguments,

And let its response data return the request parameters as is.

// Pretend this is an AXIos request function
const request = (params) = > {
  return new Promise((resolve) = > {
    setTimeout((a)= > {
      resolve(params);
    }, 1000);
  });
};
Copy the code

So the question now is how do we get the value of the View component in hoc,

How do we normally take child component instances? That’s right, ref, it’s also used here:

const withPromise = (wrapped, promiseFn) = > {
  return {
    data() { ... },
    async mounted() {
      this.loading = true;
      // Get the data from the child component instance
      const { requestParams } = this.$refs.wrapped
      // pass to the requesting function
      const result = await promiseFn(requestParams).finally((a)= > {
        this.loading = false;
      });
      this.result = result;
    },
    render(h) {
      const args = {
        props: {
          result: this.result,
          loading: this.loading,
        },
        // If you pass a ref here, you can get the child component instance, just like in the normal template.
        ref: 'wrapped'
      };

      const wrapper = h("div"[this.loading ? h("span"["Loading...") :null.this.error ? h("span"["Loading error") :null,
        h(wrapped, args),
      ]);

      returnwrapper; }}; };Copy the code

To complete the second point, when the request parameters of the child component change, the parent component also responds to resend the request and brings the new data to the child component.

const withPromise = (wrapped, promiseFn) = > {
  return {
    data() { ... },
    methods: {
      // Requests are abstracted as methods
      async request() {
        this.loading = true;
        // Get the data from the child component instance
        const { requestParams } = this.$refs.wrapped;
        // pass to the requesting function
        const result = await promiseFn(requestParams).finally((a)= > {
          this.loading = false;
        });
        this.result = result; }},async mounted() {
      // Send the request immediately, and listen for parameter changes to request again
      this.$refs.wrapped.$watch("requestParams".this.request.bind(this), {
        immediate: true}); }, render(h) { ... }}; };Copy the code

$listeners, $listeners, and $scopedSlots are all we need to do to create a child component.

$listeners are the attributes declared on the external template, and $listeners are the functions declared on the external template.

Take this example:

<my-input value="ssh" @change="onChange" />
Copy the code

Inside the component you get something like this:

{
  $attrs: {
    value: 'ssh'
  },
  $listeners: {
    change: onChange
  }
}
Copy the code

Note that the functions of $listeners are the same as those of my-Input, and that the functions of $listeners are the same as those of my-Input. $listeners can upload $attrs and $listeners directly to the el-input process.

/ / my - input inside<template>
  <el-input v-bind="$attrs" v-on="$listeners" />
</template>
Copy the code

So in the render function, we can pass it like this:

const withPromise = (wrapped, promiseFn) = > {
  return {
    ...,
    render(h) {
      const args = {
        props: {
          / / with $attrs. this.$attrs,result: this.result,
          loading: this.loading,
        },

        // Pass events
        on: this.$listeners,

        / / passed $scopedSlots
        scopedSlots: this.$scopedSlots,
        ref: "wrapped"};const wrapper = h("div"[this.loading ? h("span"["Loading...") :null.this.error ? h("span"["Loading error") :null,
        h(wrapped, args),
      ]);

      returnwrapper; }}; };Copy the code

At this point, the complete code is implemented:


      
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>hoc-promise</title>
  </head>
  <body>
    <div id="app">
      <hoc msg="msg" @change="onChange">
        <template>
          <div>I am slot</div>
        </template>
        <template v-slot:named>
          <div>I am named slot</div>
        </template>
      </hoc>
    </div>
    <script src="./vue.js"></script>
    <script>
      var view = {
        props: ["result"],
        data() {
          return {
            requestParams: {
              name: "ssh",}}; },methods: {
          reload() {
            this.requestParams = {
              name: "changed!!"}; }},template: `  {{result? .name}} < / span > < slot > < / slot > < slot name = "named" > < / slot > < button @ click = "reload" > reload data < / button > < / span > `};const withPromise = (wrapped, promiseFn) = > {
        return {
          data() {
            return {
              loading: false.error: false.result: null}; },methods: {
            async request() {
              this.loading = true;
              // Get the data from the child component instance
              const { requestParams } = this.$refs.wrapped;
              // pass to the requesting function
              const result = await promiseFn(requestParams).finally((a)= > {
                this.loading = false;
              });
              this.result = result; }},async mounted() {
            // Send the request immediately, and listen for parameter changes to request again
            this.$refs.wrapped.$watch(
              "requestParams".this.request.bind(this),
              {
                immediate: true}); }, render(h) {const args = {
              props: {
                / / with $attrs. this.$attrs,result: this.result,
                loading: this.loading,
              },

              // Pass events
              on: this.$listeners,

              / / passed $scopedSlots
              scopedSlots: this.$scopedSlots,
              ref: "wrapped"};const wrapper = h("div"[this.loading ? h("span"["Loading...") :null.this.error ? h("span"["Loading error") :null,
              h(wrapped, args),
            ]);

            returnwrapper; }}; };const request = (data) = > {
        return new Promise((r) = > {
          setTimeout((a)= > {
            r(data);
          }, 1000);
        });
      };

      var hoc = withPromise(view, request);

      new Vue({
        el: "#app".components: {
          hoc,
        },
        methods: {
          onChange() {},
        },
      });
    </script>
  </body>
</html>
Copy the code

You can preview the code here.

When we develop new components, we just take hoc and reuse it, and its business value is realized, and the code is reduced to unimaginable levels.

import { getListData } from 'api'
import { withPromise } from 'hoc'

const listView = {
  props: ["result"].template: ` 
      
    {{ item }}
`
};export default withPromise(listView, getListData) Copy the code

Everything becomes simple and elegant.

combination

Note that this chapter may be difficult for those of you who have not been exposed to React development, so read it or skip it.

One day, we were suddenly happy to write a higher-level component called withLog, which simply prints a log during mounted declaration cycles.

const withLog = (wrapped) = > {
  return {
    mounted() {
      console.log("I am mounted!")
    },
    render(h) {
      return h(wrapped)
    },
  }
}
Copy the code

Here we find that we need to extract on and scopedSlots and pass them transparently. We wrap a function that integrates the attributes we need to pass transparently from this:

function normalizeProps(vm) {
  return {
    on: vm.$listeners,
    attr: vm.$attrs,
    / / passed $scopedSlots
    scopedSlots: vm.$scopedSlots,
  }
}
Copy the code

The second argument to h is then extracted and passed.

const withLog = (wrapped) = > {
  return {
    mounted() {
      console.log("I am mounted!")
    },
    render(h) {
      return h(wrapped, normalizeProps(this))}}}Copy the code

And then wrap it outside of the hoc:

var hoc = withLog(withPromise(view, request));
Copy the code

For example, the compose function is composed from the redux library. The compose function returns a new function, which is composed to the redux library.

Functional compose

function compose(. funcs) {
  return funcs.reduce((a, b) = >(... args) => a(b(... args))) }Copy the code

Compose (a, B, c) returns a new function that nested several incoming functions

Function signature returned :(… args) => a(b(c(… args)))

It’s a function that might take a lot of time for those of you who are new to it, because it’s really complicated, but once you understand it, you take your functional thinking to the next level.

For example, compose uncompose is composed with multiple parameters. For example, compose uncompose is composed with multiple parameters

Circulating compose

For those of you who are not aware of the compose function, we can write a regular loop that returns a function, executes the array of incoming functions from right to left, and takes the return value of the last function as an argument to the next one.

The compose function should look something like this:

function compose(. args) {
  return function(arg) {
    let i = args.length - 1
    let res = arg
    while(i >= 0) {
     let func = args[i]
     res = func(res)
     i--
    }
    return res
  }
}
Copy the code

Transform withPromise

However, this means that we need to modify the higher-order withPromise function, because if you look closely at compose, it wraps the function, makes it accept an argument, and passes the return value of the first function to the next function as an argument.

For example, for compose(a, b), the value returned by b(arg) will be used as a parameter to the compose(b(args) call.

This requires making sure that each of the functions that compose accepts takes only one argument.

So along the same lines, we modify withPromise to further advance it so that it returns a function that takes only one argument:

const withPromise = (promiseFn) = > {
  // Return a layer of function wrap, which takes only one argument
  return function wrap(wrapped) {
    // Go one layer further and return the component
    return {
      mounted() {},
      render() {},
    }
  }
}
Copy the code

With it, you can compose higher-order components more elegantly:

const compsosed = compose(
    withPromise(request),
    withLog,
)

const hoc = compsosed(view)
Copy the code

The full code for the compose section above is here.

Note that this section is normal if you don’t understand these concepts for the first time. They are popular in the React community, but rarely discussed in the Vue community! About the compose function, in the React community come into contact with it for the first time I can’t understand completely, to know its usage, slowly understanding is not late.

Real business Scenario

Many people may find the above code to be of little practical value, but the vue-Router advanced usage documentation actually shows a scenario where higher-order components are used to solve the problem.

To briefly describe the scenario, we know that vue-Router can be configured with asynchronous routes. However, in the case of slow network speed, the chunk corresponding to this asynchronous route, namely component code, will be jumped after the download is completed.

During the Loading of asynchronous components, we want to show a Loading component to make the interaction more friendly.

In the section of Vue document — Asynchronous components, it can be clearly seen that Vue supports the rendering component corresponding to asynchronous component declaration loading:

const AsyncComponent = (a)= > ({
  // The component to load (should be a 'Promise' object)
  component: import('./MyComponent.vue'),
  // The component used when the asynchronous component is loaded
  loading: LoadingComponent,
  // The component used when loading failed
  error: ErrorComponent,
  // Display the component delay time when loading. The default is 200 (ms)
  delay: 200.// If a timeout is provided and the component loads time out,
  // Use the component used when the load failed. The default value is' Infinity '
  timeout: 3000
})
Copy the code

We tried writing this code to vue-router to override the original asynchronous route:

new VueRouter({
    routes: [{
        path: '/',
- component: () => import('./MyComponent.vue')
+ component: AsyncComponent}]})Copy the code

It is not supported at all. After debugging the source code of VUe-Router, it is found that vue-Router has two different logic for parsing asynchronous components and processing vue. In the implementation of Vue-Router, it will not help you to render Loading components.

This is certainly not difficult for the intelligent community leaders, we change a way of thinking, let vue-Router jump to a container component, this container component helps us to use vue internal rendering mechanism to render AsyncComponent, can not render the loading state? The specific code is as follows:

Since vue-Router’s Component field accepts a Promise, we wrap the component in promise.resolve.

function lazyLoadView (AsyncView) {
  const AsyncHandler = (a)= > ({
    component: AsyncView,
    loading: require('./Loading.vue').default,
    error: require('./Timeout.vue').default,
    delay: 400.timeout: 10000
  })

  return Promise.resolve({
    functional: true,
    render (h, { data, children }) {
      // Here we use vUE's internal rendering mechanism to render true asynchronous components
      return h(AsyncHandler, data, children)
    }
  })
}
  
const router = new VueRouter({
  routes: [{path: '/foo'.component: (a)= > lazyLoadView(import('./Foo.vue'))}]})Copy the code

In this way, a nice Loading component is rendered on the page in between Loading code on the jump.

conclusion

All of the code for this article is stored in the Github repository and previewed.

This literature to study in my source gave me great help on the road of the Vue technology insider author hcysun bosses, although I haven’t spoken to him, but when I was a few months working on the small white, a business needs to think: let I found this article explore the Vue high-order component | HcySunYang

At that time, I could not understand the source code problems and fixes mentioned in this article, and then switched to a different way to implement the business, but the article mentioned in my mind has always been, I am busy working in the spare time to learn the source code, in the hope that one day I can fully understand this article.

This bug was rewritten in Vue 2.6 due to the slot implementation, and it was also fixed. Now Vue uses the latest slot syntax in conjunction with higher-order functions. The bugs mentioned in this article are no longer encountered.

❤️ thank you

1. If this article is helpful to you, please support it with a like. Your like is the motivation for my writing.

2. Follow the public account “front-end from advanced to hospital” to add my friends, I pull you into the “front-end advanced communication group”, we communicate and progress together.