How to Reuse the Logic of a Component

Keywords: Javascript Vue React Attribute Programming

Preface

This paper briefly discusses the history of logical combination and reuse modes of React and Vue mainstream view libraries: from Mixins to HOC, then to Render Props, and finally to the latest Hooks.

* Note: JS script files are introduced globally in this article, so you will see: const {createElement: h} = React; object deconstruction like this, not ES Modules import. Also, please read the notes!

The full text consists of 22560 words. It takes about 45 minutes to complete the reading.

Mixins

Mixing in Object-Oriented

Mixers is a very popular pattern of logical reuse in traditional object-oriented programming. Its essence is the copy of attributes/methods, such as the following example:

const eventMixin = {
  on(type, handler) {
    this.eventMap[type] = handler;
  },
  emit(type) {
    const evt = this.eventMap[type];
    if (typeof evt === 'function') {
      evt();
    }
  },
};

class Event {
  constructor() {
    this.eventMap = {};
  }
}

// Copy the attribute method in mixin to the Event prototype object
Object.assign(Event.prototype, eventMixin);

const evt = new Event();
evt.on('click', () => { console.warn('a'); });
// Trigger click event after 1 second
setTimeout(() => {
  evt.emit('click');
}, 1000);

Mixing in Vue

In Vue, mixins can contain options that all component instances can pass in, such as data, computed, and mounted lifecycle hook functions. The strategy of conflict merging with the same name is that the option of value as object takes precedence over component data, the life cycle hook function of the same name is called, and the life cycle hook function of mixin is called before the component.

const mixin = {
  data() {
    return { message: 'a' };
  },
  computed: {
    msg() { return `msg-${this.message}`; }
  },
  mounted() {
    // What do you think the print results of these two attribute values will be?
    console.warn(this.message, this.msg);
  },
};

new Vue({
  // Why add the non-empty el option? Because if the root instance has no el option, it will not trigger the mounted lifecycle hook function. You can try to set it to a null value or change mounted to create.
  el: '#app',
  mixins: [mixin],
  data() {
    return { message: 'b' };
  },
  computed: {
    msg() { return `msg_${this.message}`; }
  },
  mounted() {
    // The message attribute in data has been merge, so the print is b; so is the MSG attribute, which is msg_b.
    console.warn(this.message, this.msg);
  },
});

From the conflicting merging strategy of the same name of mixins, it is not difficult to see that adding mixins to components requires some special processing, and adding many mixins inevitably leads to performance loss.

Mixing in React

In React, mixin s have been removed with the createClass method in version 16, but we can also find a 15 version to see:

// If the comment is removed, it will be wrong. React does not automatically merge the options that are valued as objects, but reminds developers not to declare the same name attribute.
const mixin = {
  // getInitialState() {
  //   return { message: 'a' }; 
  // },
  componentWillMount() {
    console.warn(this.state.message);
    this.setData();
  },
  // setData() {
  //   this.setState({ message: 'c' });
  // },
};

const { createElement: h } = React;
const App = React.createClass({
  mixins: [mixin],
  getInitialState() {
    return { message: 'b' }; 
  },
  componentWillMount() {
    // For the lifecycle hook function merging strategy, Vue and React are the same: life cycle hook functions with the same name are called, and life cycle hook functions in mixin s are called before components.
    console.warn(this.state.message);
    this.setData();
  },
  setData() {
    this.setState({ message: 'd' });
  },
  render() { return null; },
});

ReactDOM.render(h(App), document.getElementById('app'));

Defects of Mixins

  • First, Mixins introduces implicit dependencies, especially when multiple mixins or even nested mixins are introduced, the source of attributes / methods in components is very unclear.
  • Secondly, Mixins may cause namespace conflicts. All introduced mixins are located in the same namespace. The attributes/methods introduced by the former mixin are overwritten by the same-name attributes/methods of the latter mixin, which is particularly unfriendly for projects that refer to third-party packages.
  • Nested Mixins are interdependent and mutually coupled, leading to snowball complexity, which is not conducive to code maintenance.

Okay, that's all about mixin in this article. If you're tired, you might as well take a rest. There's a lot more to follow.

HOC

Higher order function

Let's first look at higher-order functions and Wikipedia's concepts.

In mathematics and computer science, higher-order functions are functions that satisfy at least one of the following conditions: accepting one or more functions as input and outputting one function

map functions found in many functional programming languages are an example of higher-order functions. It accepts a function f as a parameter and returns a function that accepts a list and applies f to each element of it. In functional programming, higher-order functions that return another function are called Curry ized functions.

For example (please ignore that I did not type check):

function sum(...args) {
  return args.reduce((a, c) => a + c);
}

const withAbs = fn => (...args) => fn.apply(null, args.map(Math.abs));
// The full use of the arrow function may impose a burden on the understanding of unfamiliar people, but it is still very common, in fact, equivalent to the following writing.
// function withAbs(fn) {
//   return (...args) => {
//     return fn.apply(null, args.map(Math.abs));
//   };
// }

const sumAbs = withAbs(sum);
console.warn(sumAbs(1, 2, 3));
console.warn(sumAbs(1, -2));

HOC in React

According to the above concept, a higher-order component is a function that accepts a component function and outputs a component function as Curry. The most classic example of HOC is to wrap a layer of loading state for a component, such as:

For some slow-loading resources, the component initially shows the standard Loading effect, but after a certain period of time (for example, 2 seconds), it becomes a friendly reminder that "resources are larger and are actively loading, please wait a moment", and the specific content will be displayed after the resource is loaded.

const { createElement: h, Component: C } = React;

// The input of HOC can be expressed in such a simple way.
function Display({ loading, delayed, data }) {
  if (delayed) {
    return h('div', null, 'Big resources, active loading, please wait a minute');
  }
  if (loading) {
    return h('div', null, 'Loading');
  }

  return h('div', null, data);
}
// A higher-order component is a function that accepts a component function and outputs a component function as a Curry function.
const A = withDelay()(Display);
const B = withDelay()(Display);

class App extends C {
  constructor(props) {
    super(props);
    this.state = {
      aLoading: true,
      bLoading: true,
      aData: null,
      bData: null,
    };
    this.handleFetchA = this.handleFetchA.bind(this);
    this.handleFetchB = this.handleFetchB.bind(this);
  }
  
  componentDidMount() {
    this.handleFetchA();
    this.handleFetchB();
  }

  handleFetchA() {
    this.setState({ aLoading: true });
    // Resource loading is completed in 1 second and does not trigger load prompt text switching
    setTimeout(() => {
      this.setState({ aLoading: false, aData: 'a' });
    }, 1000);
  }

  handleFetchB() {
    this.setState({ bLoading: true });
    // It takes 7 seconds to load the resource, and 5 seconds to load the prompt text switch after the request starts.
    setTimeout(() => {
      this.setState({ bLoading: false, bData: 'b' });
    }, 7000);
  }
  
  render() {
    const {
      aLoading, bLoading, aData, bData,
    } = this.state;
    
    return h('article', null, [
      h(A, { loading: aLoading, data: aData }),
      h(B, { loading: bLoading, data: bData }),
      // After reloading, the logic of loading prompt text cannot be changed
      h('button', { onClick: this.handleFetchB, disabled: bLoading }, 'click me'),
    ]);
  }
}

// Switch load prompt text after default 5 seconds
function withDelay(delay = 5000) {
  // So how to implement this higher-order function? Readers can write their own first.
}

ReactDOM.render(h(App), document.getElementById('app'));

Written roughly as follows:

function withDelay(delay = 5000) {
  return (ComponentIn) => {
    class ComponentOut extends C {
      constructor(props) {
        super(props);
        this.state = {
          timeoutId: null,
          delayed: false,
        };
        this.setDelayTimeout = this.setDelayTimeout.bind(this);
      }

      componentDidMount() {
        this.setDelayTimeout();
      }

      componentDidUpdate(prevProps) {
        // When loading is completed/reloaded, clean up the old timer and set up a new timer
        if (this.props.loading !== prevProps.loading) {
          clearTimeout(this.state.timeoutId);
          this.setDelayTimeout();
        }
      }

      componentWillUnmount() {
        clearTimeout(this.state.timeoutId);
      }

      setDelayTimeout() {
        // delayed needs to be reset after loading / reloading
        if (this.state.delayed) {
          this.setState({ delayed: false });
        }
        // Setting the timer only when it is in the loading state
        if (this.props.loading) {
          const timeoutId = setTimeout(() => {
            this.setState({ delayed: true });
          }, delay);
          this.setState({ timeoutId });
        }
      }
      
      render() {
        const { delayed } = this.state;
        // Transparent props
        return h(ComponentIn, { ...this.props, delayed });
      }
    }
    
    return ComponentOut;
  };
}

HOC in Vue

The idea of implementing HOC in Vue is the same, but the component of input/output in Vue is not a function or class, but a JavaScript object with template/render options:

const A = {
  template: '<div>a</div>',
};
const B = {
  render(h) {
    return h('div', null, 'b');
  },
};

new Vue({
  el: '#app',
  render(h) {
    // When the first parameter of the rendering function is not a string type, you need to be a JavaScript object that contains the template/render option
    return h('article', null, [h(A), h(B)]);
  },
  // In template writing, components need to be registered in instances
  // components: { A, B },
  // template: `
  //   <article>
  //     <A />
  //     <B />
  //   </artcile>
  // `,
});

Therefore, the input of HOC in Vue needs to be expressed as follows:

const Display = {
  // For the sake of brevity, there is no type detection and default settings.
  props: ['loading', 'data', 'delayed'],
  render(h) {
    if (this.delayed) {
      return h('div', null, 'Over-resourced, trying to load');
    }
    if (this.loading) {
      return h('div', null, 'Loading');
    }

    return h('div', null, this.data);
  },
};
// Almost exactly the same way.
const A = withDelay()(Display);
const B = withDelay()(Display);

new Vue({
  el: '#app',
  data() {
    return {
      aLoading: true,
      bLoading: true,
      aData: null,
      bData: null,
    };
  },
  mounted() {
    this.handleFetchA();
    this.handleFetchB();
  },
  methods: {
    handleFetchA() {
      this.aLoading = true;
      // Resource loading is completed in 1 second and does not trigger load prompt text switching
      setTimeout(() => {
        this.aLoading = false;
        this.aData = 'a';
      }, 1000);
    },

    handleFetchB() {
      this.bLoading = true;
      // The resource needs 7 seconds to load, and the prompt text switch will be loaded 5 seconds after the request starts.
      setTimeout(() => {
        this.bLoading = false;
        this.bData = 'b';
      }, 7000);
    },
  },
  render(h) {
    return h('article', null, [
      h(A, { props: { loading: this.aLoading, data: this.aData } }),
      h(B, { props: { loading: this.bLoading, data: this.bData } }),
      // After reloading, the logic of loading prompt text cannot be changed
      h('button', {
        attrs: {
          disabled: this.bLoading,
        },
        on: {
          click: this.handleFetchB,
        },
      }, 'click me'),
    ]);
  },
});

The withDelay function is also easy to write:

function withDelay(delay = 5000) {
  return (ComponentIn) => {
    return {
      // If the props of ComponentIn and ComponentOut are exactly the same, you can use the `props: ComponentIn.props'.
      props: ['loading', 'data'],
      data() {
        return {
          delayed: false,
          timeoutId: null,
        };
      },
      watch: {
        // Replace component DidUpdate with watch
        loading(val, oldVal) {
          // When loading is completed/reloaded, clean up the old timer and set up a new timer
          if (oldVal !== undefined) {
            clearTimeout(this.timeoutId);
            this.setDelayTimeout();
          }
        },
      },
      mounted() {
        this.setDelayTimeout();
      },
      beforeDestroy() {
        clearTimeout(this.timeoutId);
      },
      methods: {
        setDelayTimeout() {
          // delayed needs to be reset after loading / reloading
          if (this.delayed) {
            this.delayed = false;
          }
          // Setting the timer only when it is in the loading state
          if (this.loading) {
            this.timeoutId = setTimeout(() => {
              this.delayed = true;
            }, delay);
          }
        },
      },
      render(h) {
        const { delayed } = this;
        // Transparent props
        return h(ComponentIn, {
          props: { ...this.$props, delayed },
        });
      },
    };
  };
}

Nested HOC

Here we use React as an example:

const { createElement: h, Component: C } = React;

const withA = (ComponentIn) => {
  class ComponentOut extends C {
    renderA() {
      return h('p', { key: 'a' }, 'a');
    }
    render() {
      const { renderA } = this;
      return h(ComponentIn, { ...this.props, renderA });
    }
  }

  return ComponentOut;
};

const withB = (ComponentIn) => {
  class ComponentOut extends C {
    renderB() {
      return h('p', { key: 'b' }, 'b');
    }
    // Existence of homonymous functions in HOC
    renderA() {
      return h('p', { key: 'c' }, 'c');
    }
    render() {
      const { renderB, renderA } = this;
      return h(ComponentIn, { ...this.props, renderB, renderA });
    }
  }

  return ComponentOut;
};

class App extends C {
  render() {
    const { renderA, renderB } = this.props;
    return h('article', null, [
      typeof renderA === 'function' && renderA(),
      'app',
      typeof renderB === 'function' && renderB(),
    ]);
  }
}

// What do you think renderA returns? With A (with B (App)?
const container = withB(withA(App));

ReactDOM.render(h(container), document.getElementById('app'));

So it is not difficult to see that for HOC, props also have naming conflicts. Similarly, when multiple HOCs are introduced or even nested HOCs are introduced, the property/method source of prop in components is very unclear.

Advantages and Defects of HOC

First of all, defect:

  • First, like Mixins, HOC props introduce implicit dependencies. When multiple HOCs or even nested HOCs are introduced, the source of prop's attributes/methods in components is very unclear.
  • Secondly, props of HOC may cause namespace conflicts, and homonymous attributes/methods of prop will be overwritten by HOC executed later.
  • HOC requires additional nesting of component instances to encapsulate logic, resulting in unnecessary performance overhead

Again, advantages:

  • HOC is a pure function without side effects. Nested HOCs are not interdependent and coupled.
  • The output component does not share the state with the input component, nor can it directly modify the state of the output component using its own setState, which ensures that the source of state modification is single.

You may want to know that HOC hasn't solved too many problems brought by Mixins. Why not continue using Mixins?

A very important reason is that components defined based on class/function grammar need to be instantiated before they can copy attributes/methods in mixins into components. Developers can copy them themselves in constructors, but it is difficult for class libraries to provide such a mixins option.

Well, that's all about HOC in this article. This article does not introduce the use of HOC notes / compose functions and other knowledge points, unfamiliar readers can read React Official documents (escape)

Render Props

Render Props in React

In fact, you have seen the use of Render Props in the nested HOC section above. The essence of Render Props is to pass rendering functions to subcomponents:

const { createElement: h, Component: C } = React;

class Child extends C {
  render() {
    const { render } = this.props;
    return h('article', null, [
      h('header', null, 'header'),
      typeof render === 'function' && render(),
      h('footer', null, 'footer'),
    ]);
  }
}

class App extends C {
  constructor(props) {
    super(props);
    this.state = { loading: false };
  }
  
  componentDidMount() {
    this.setState({ loading: true });
    setTimeout(() => {
      this.setState({ loading: false });
    }, 1000);
  }
  renderA() { return h('p', null, 'a'); }
  renderB() { return h('p', null, 'b'); }

  render() {
    const render = this.state.loading ? this.renderA : this.renderB;
    // Of course, you can also call it render, as long as the rendering function is passed to the sub-component accurately.
    return h(Child, { render });
  }
}

ReactDOM.render(h(App), document.getElementById('app'));

slot in Vue

In Vue, the concept of Render Props corresponds to slots or, in general, Renderless Components.

const child = {
  template: `
    <article>
      <header>header</header>
      <slot></slot>
      <footer>footer</footer>
    </article>
  `,
  // The template is well understood and the rendering function is as follows:
  // render(h) {
  //   return h('article', null, [
  //     h('header', null, 'header'),
  //     // Because no named slot is used, all Vnode s are fetched directly by default.
  //     this.$slots.default,
  //     h('footer', null, 'footer'),
  //   ]);
  // },
};

new Vue({
  el: '#app',
  components: { child },
  data() {
    return { loading: false };
  },
  mounted() {
    this.loading = true;
    setTimeout(() => {
      this.loading = false;
    }, 1000);
  },
  template: `
    <child>
      <p v-if="loading">a</p>
      <p v-else>b</p>
    </child>
  `,
});

It's easy to see that in Vue, we don't need to explicitly pass the rendering function, and the library will automatically pass through $slots.

Limited to space, Vue 2.6 before the writing: slot and slot-scope are not introduced here, the reader can read Vue Official documents Here we introduce the writing of v-slot:

const child = {
  data() {
    return {
      obj: { name: 'obj' },
    };
  },
  // The attributes bound on slot can be passed to the parent component and received by `v-slot:[name]= `slotProps'. Of course, slotProps can be named by other names, and can also be written in the following way of object deconstruction.
  template: `
    <article>
      <header>header</header>
      
      <slot name="content"></slot>
      <slot :obj="obj"></slot>
      
      <footer>footer</footer>
    </article>
  `,
};
    
new Vue({
  el: '#app',
  components: { child },
  data() {
    return { loading: false };
  },
  mounted() {
    this.loading = true;
    setTimeout(() => {
      this.loading = false;
    }, 1000);
  },
  // # Content is short for v-slot:content
  template: `
    <child>
      <template #content>
        <p v-if="loading">a</p>
        <p v-else>b</p>  
      </template>

      <template #default="{ obj }">
        {{ obj.name }}
      </template>
    </child>
  `,
});

It should be noted that unlike slot, v-slot can only be added to <template> instead of arbitrary tags.

Advantages and Disadvantages of Render Props

Just like the name of this pattern, Render Props is only a usage of component prop. In order to reuse logic, it is necessary to encapsulate the operation of state/view into the rendering function of prop, so it will cause performance loss as well as HOC. However, because prop has only one attribute, it will not cause the conflict of HOC prop names.

All right, that's all about Render Props in this article. Finally, we'll introduce the best component logic combination and reuse mode Hooks.

Hooks

Hooks in React

Hooks was officially introduced in React version 16.8. Let's first look at the hook useState of the operation state:

const { createElement: h, useState } = React;

function App() {
  // No super(props), no this.onClick = this.onClick.bind(this)
  const [count, setCount] = useState(0);

  function onClick() {
    // No this.state.count, no this.setState
    setCount(count + 1);
  }

  return h('article', null, [
    h('p', null, count),
    h('button', { onClick }, 'click me'),
  ]);
}

ReactDOM.render(h(App), document.getElementById('app'));

There are no life cycle function hooks in the function, so React Hooks provides a hook useEffect with operational side effects, and the callback in the hook is called after rendering is complete.

const { createElement: h, useState, useEffect } = React;

function App() {
  const [message, setMessage] = useState('a');
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    // The second parameter of `useEffect'is not specified. callback is called after each rendering, so clicking the button will print the use effect all the time.
    console.warn('use effect', count);
    setTimeout(() => {
      setMessage('b');
    }, 1000);

    // The function returned in useEffect will be called before the next effect starts after the rendering is complete
    return () => {
      console.warn('clean up', count);
    };
  });

  useEffect(() => {
    // Tell React that your effect does not depend on any value in props or state, so it never needs to be repeated, which is equivalent to componentDidMount.
  }, []);
  // An empty array can be replaced by an array of variables that state does not change.
  // const [fake] = useState(0);
  // useEffect(() => {}, [fake]);
  
  useEffect(() => {
    return () => {
      // Equivalent to componentWillUnmount
    };
  }, []);

  console.warn('render', count);

  return h('article', null, [
    h('p', null, count),
    h('button', { onClick }, 'click me'),
    h('p', null, message),
  ]);
}

ReactDOM.render(h(App), document.getElementById('app'));

In addition to the two most commonly used hooks, React Hooks also provides many built-in hook functions. Here is an example of useCallback:

const { createElement: h, useCallback } = React;

function useBinding(initialValue) {
  const [value, setValue] = useState(initialValue);

  // The function of bidirectional binding can be easily realized by using useCallback
  const onChange = useCallback((evt) => {
    setValue(evt.target.value);
  }, [value]);

  return [value, onChange];
}

function App() {
  const [value, onChange] = useBinding('text');

  return h('article', null, [
    h('p', null, value),
    h('input', { value, onChange }),
  ]);
}

Well, we know the basic usage of Hooks. So how to rewrite the example of HOC with Hooks?

For some slow-loading resources, the component initially shows the standard Loading effect, but after a certain period of time (for example, 2 seconds), it becomes a friendly reminder that "resources are larger and are actively loading, please wait a moment", and the specific content will be displayed after the resource is loaded.

Looking closely at the withDelay function above, it is not difficult to find that at the component level, we just pass a prop named delayed to the input component.

So for Hooks, we can make sure that the view component Display and the root component App remain unchanged, just modify the HOC with Delay to customize HookuseDelay, which returns only the delayed variable.

function useDelay({ loading, delay = 5000 }) {
  // Customizing Hook, you need to return a delayed variable
}

function HookDisplay(props) {
  const delayed = useDelay({ loading: props.loading });
  // See the HOC section in React above for Display component functions
  return h(Display, { ...props, delayed });
}

// Since the two components in the example are identical except props, they share a component function (as you can see from a close look at the HOC example)
const A = HookDisplay;
const B = HookDisplay;
// Can you do anything with a simpler function?
function useDelay({ loading, delay = 5000 }) {
  const [delayed, setDelayed] = useState(false);

  useEffect(() => {
    // delayed needs to be reset after loading / reloading
    if (delayed) {
      setDelayed(false);
    }
    // Setting the timer only when it is in the loading state
    const timeoutId = loading ? setTimeout(() => {
      setDelayed(true);
    }, delay) : null;

    return () => {
      clearTimeout(timeoutId);
    };
  }, [loading]);

  return delayed;
}

Composition API in Vue

Hooks in Vue is called the Composition API proposal, and the API is not stable at present, so the following content may be changed.

Similarly, let's first look at the Hook of the operation state. Vue provides two Hooks of the operation state, ref and reactive (in the previous RFC, they were called value and state, respectively):

<main id="app">
  <p>{{ count }}</p>
  <button @click="increment">click me</button>
</main>

<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
<script src="https://unpkg.com/@vue/composition-api/dist/vue-composition-api.umd.js"></script>

<script>
  // VueComposition Api. default is an object that contains the install attribute (that is, a plug-in for Vue)
  const { ref, default: VueCompositionApi } = vueCompositionApi;
  Vue.use(VueCompositionApi);

  new Vue({
    el: '#app',
    setup() {
      // You will find that count is a responding object with only one value attribute pointing to its internal value. Variables declared by ref are called wrappers
      // The wrapper object is automatically expanded when used in the template, that is, `count'can be used directly without writing `count.value'.`
      const count = ref(0);
      
      function increment() {
        // This requires a very subtle addition of `value', which is one of the reasons why Vue decided to rename `value'to `ref'. (It's not intuitive to call the value function to return an object that contains the value attribute.)
        count.value += 1;
      }

      return { count, increment };
    },
  });
</script>

It is worth noting that there are still some differences between the ref hook of Vue and the useRef hook of React. UseRef is not essentially a hook of operation state (or the state of operation does not affect the view).

const { createElement: h, useRef } = React;

function App() {
  const count = useRef(0);
  function onClick() {
    // Although the same ref object is returned for each rendering, changing the current property does not result in component re-rendering
    console.warn(count.current);
    count.current += 1;
  }      

  return h('article', null, [
    h('p', null, count.current),
    h('button', { onClick }, 'click me'),
  ]);
}

ReactDOM.render(h(App), document.getElementById('app'));
<main id="app">
  <p>{{ state.count }}</p>
  <p>{{ state.double }}</p>
  
  <button @click="increment">click me</button>
</main>

<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
<script src="https://unpkg.com/@vue/composition-api/dist/vue-composition-api.umd.js"></script>

<script>
  const { default: VueCompositionApi, reactive, computed } = vueCompositionApi;
  Vue.use(VueCompositionApi);

  new Vue({
    el: '#app',
    setup() {
      const state = reactive({
        count: 0,
        double: computed(() => state.count * 2),
      });
      // For value attributes, it can be directly replaced by Vue.observable
      // For computing attributes, vueComposition Api. computed returns a wrapped object that needs to be processed. Readers can print state.double without annotations.
      // const state = Vue.observable({
      //   count: 0,
      //   double: computed(() => state.count * 2),
      // });
      function increment() {
        state.count += 1;
      }

      return { state, increment };
    },
  });
</script>

React Hooks is invoked every time a component is rendered, and the state is implicitly mounted on the current internal component node, which is extracted according to the invocation order in the next rendering. The setup() of Vue is invoked only once at initialization, and the state is stored in the closure of setup() by reference.

Therefore, Vue does not directly provide the hook of operation side effects. It still provides the hook of life cycle function. Besides adding on prefix, there is not much difference between the former and the latter. Take onMounted as an example:

<main id="app">
  <ul>
    <li v-for="item in list">{{ item }}</li>
  </ul>
</main>

<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
<script src="https://unpkg.com/@vue/composition-api/dist/vue-composition-api.umd.js"></script>

<script>
  const { default: VueCompositionApi, reactive, onMounted } = vueCompositionApi;
  Vue.use(VueCompositionApi);

  new Vue({
    el: '#app',
    setup() {
      const list = reactive([1, 2, 3]);
      onMounted(() => {
        setTimeout(() => {
          list.push(4);
        }, 1000);
      });

      return { list };
    },
  });
</script>

The migration of the HOC example to the Composition API in the previous section requires little modification, leaving the Display component object and the root Vue instance options unchanged:

function useDelay(props, delay = 5000) {
  // Customizing Hook, you need to return a delayed variable
}

const HookDisplay = {
  props: ['loading', 'data'],
  setup(props) {
    const delayed = useDelay(props);
    return { delayed };
  },
  render(h) {
    // See the HOC section in Vue above for Display component objects
    return h(Display, {
      props: {
        ...this.$props, delayed: this.delayed,
      },
    });
  },
};

const A = HookDisplay;
const B = HookDisplay;
const {
  default: VueCompositionApi, ref, watch, onMounted, onUnmounted,
} = vueCompositionApi;
Vue.use(VueCompositionApi);

function useDelay(props, delay = 5000) {
  const delayed = ref(false);
  let timeoutId = null;

  // You can try changing props for loading.
  // Because loading is the basic type, the ability to respond (no longer the object's getter/setter) is lost when passing parameters.
  watch(() => props.loading, (val, oldVal) => {
    if (oldVal !== undefined) {
      clearTimeout(timeoutId);
      setDelayTimeout();
    }
  });
  onMounted(() => {
    setDelayTimeout();
  });
  onUnmounted(() => {
    clearTimeout(timeoutId);
  });

  function setDelayTimeout() {
    if (delayed.value) {
      delayed.value = false;
    }
    if (props.loading) {
      timeoutId = setTimeout(() => {
        delayed.value = true;
      }, delay);
    }
  }

  return delayed;
}

Advantages of Hooks

It's not hard to see that Hooks and Render Props have similar ideas, but Render Props returns components and Hooks returns some states (which you need to pass on to components yourself). Thanks to the fine-grained encapsulation capability of Hooks, rendering functions no longer need to be passed through components, which corrects the defect that Render Props requires additional nesting of component instances to encapsulate logic.

All right, that's all about the logical combination and reuse mode in this paper. There are inevitably omissions and errors in the text, and readers are expected to criticize and correct them.

Posted by yazz on Sun, 22 Sep 2019 04:20:30 -0700