Author: Changfeng

First heard of React Hooks, the Internet was flooded with articles about them in the days after their release. The React hook is used to modify the React component. React extends a wide range of concepts, including higher-order components, functions, Render Props, Context, and so on. Here’s another concept, front-end development is complicated enough already! I have been using Vue for the past two years. I think many features related to React have similar solutions in Vue, so I didn’t know about it immediately.

I later saw that Utah also mentioned the Hooks API in the recent Vue 3.0 progress video and wrote a POC that uses Hooks in Vue. It seems that Hooks are important, so I immediately found the official React Hooks document and the video of the press conference — another round of fixes.

Hooks are a promising solution to a number of current front-end development problems. However, React Hooks are still in alpha, not perfect, and built-in Hooks are not as rich. Vue only has Hooks POC, Vue3.0 will probably add them, but it will take a few more months. Therefore, it is not recommended to use it in formal code.

This article focuses on my understanding of Hooks and the source implementation of the Hooks API in Vue. Hooks are a neutral concept that can be used in any framework, not unique to React.

What problem do Hooks solve

Before we begin, let’s repeat what Hooks help us solve.

According to Dan, the React project encountered the following pain points:

  1. Cross-component code that is difficult to reuse.
  2. Large components, difficult to maintain.
  3. Component tree hierarchy, often deeply nested.
  4. Class component, not easy to understand.

Of course, the Vue project is the same, and these issues are actually related.

Component-based development, we split the page into different components, according to the top-down data flow, layer upon layer nesting. The smallest grain of code structure is the component.

If some component is too big, we continue to break it into smaller components and call it in the parent component. If there is a lot of common logic between components, we use mixins or build component inheritance.

The problem is that component splitting makes it easy to inadvertently layer components too deeply, adding complexity to the system and potentially affecting performance. In addition, the interaction logic of some components is indeed quite complex, which cannot be disassembled. After long-term iteration of the system, the accumulated code amount is very large, and the associated logic is scattered in different life cycles of components, which is difficult to maintain.

Reuse across component logic is even trickier! Mixins are a double-edged sword (see: mixins are harmful); Component inheritance is also undesirable. Although it works well in strongly typed object-oriented languages (e.g. Java/C#), it is often difficult to implement in JavaScript and makes code difficult to understand. Extracting the util package is also a common practice, but if the public logic to be extracted needs to be associated with the local state of the component, and if the associated public logic needs to be scattered across different life cycles of the component, it can’t be resolved. At this point, we tend to compromise — the big component/repeat logic is born.

Class components are also a mix of love and hate, and we’ve long advocated abstracting code structure in an object-oriented way, which is a good choice until there’s a better solution. But I personally don’t think it works well in JavaScript, especially in the React/Vue component architecture. We often need clever tricks to enable JavaScript types to support super, private members, and careful handling of the reference to this in functions. Thanks to the flexibility and expressiveness of JavaScript, you can always find the right way to write code. The question is, how do you maintain such code? We want it to be concise and conventionally written, not obscure and full of minefields. This alone, we know that JavaScript is statically scoped, that is, from the source code, we can infer the scope of a variable, but this is an exception, it is dynamically scoped, that is, the value of this is determined by the caller. The same method, called in different ways, can point to completely different versions of this, forcing us to make extensive use of bind to ensure that this points.

For those of you who are interested in the use of this in JavaScript, please refer to: detail this.

How to fix these pain points — Hooks!

What is the Hooks

The definition on Wikipedia for hooks is:

The term hooking covers a range of techniques used to alter or augment the behavior of an operating system, of applications, or of other software components by intercepting function calls or messages or events passed between software components. Code that handles such intercepted function calls, events or messages is called a hook.

Hooks are a set of techniques that change or enhance the behavior of operating systems, applications, or software components. These techniques are implemented by intercepting function calls, messages, and events during software execution.

That is, with Hooks, we can later change or enhance the runtime behavior of existing systems. So, in the React/Vue component system, Hooks are code modules that change or enhance the runtime behavior of components.

By reading the React Hooks technical documentation, it is. React provides two important built-in Hooks:

  • UseState — Adds local responsive state to the component.
  • UseEffect – Side effect logic that needs to be executed after a status update is added to a component.

React also provides several other built-in Hooks related to component features, such as useContext, useReducer, useMemo, useRef, and so on. There should be more built-in Hooks in the future that cut into every aspect of a component’s runtime. We can also implement custom Hooks based on these built-in Hooks.

React emphasizes that Hooks can only be used in functional components. A functional component is essentially a pure rendering function, stateless, with data from outside. So how do you add local state to your components, along with the various life-cycle related business logic? The answer is: through Hooks. The React team wants “functional components + Hooks” to be the primary way to develop components in the future, so Hooks should have the ability to hack into every part of the component lifecycle to add state and behavior to components. While the Hooks provided by React are not as extensive as they should be, they will be improved over time.

Taken together, we found that Hooks allowed us to make our modular development more granular and functional. Component functionality is now pieced together by Hooks. Such features also solve the four pain points mentioned above: code reuse, large components, component tree too deep, class components.

For background on React Hooks and many examples, see: Introducing Hooks

For Vue, in addition to useState, useEffect, useRef, and React Hooks API, you can also implement useComputed, useMounted, useUpdated, useWatch, and other built-in Hooks. So that you can add functionality to the component in more detail.

Vue implementation of the Hooks API

To deepen your understanding of the Hooks API, here’s a look at the source code implementation of the Hooks POC of Vue in particular.

withHooks

We know that React Hooks can only be used in functional components and are defined this way in Vue.

WithHooks is used to wrap a Vue version of a “functional component” in which you can use the Hooks API.

WithHooks Examples of using this function:

import { withHooks, useData, useComputed } from "vue-hooks"

const Foo = withHooks(h= > {
  const data = useData({
    count: 0
  })
  const double = useComputed((a)= > data.count * 2)
  return h('div', [
    h('div'.`count is ${data.count}`),
    h('div'.`double count is ${double}`),
    h('button', { on: { click: (a)= > {
      data.count++
    }}}, 'count++')])})Copy the code

This code wraps a functional component (rendering function) withHooks, which add a local state data and a compute property double to the component.

Note: useData in the code is similar to useState, as explained below.

WithHooks Implementation details:

let currentInstance = null
let isMounting = false
let callIndex = 0

function ensureCurrentInstance() {
  if(! currentInstance) {throw new Error(
      `invalid hooks call: hooks can only be called in a function passed to withHooks.`)}}export function withHooks(render) {
  return {
    data() {
      return {
        _state: {}
      }
    },
    created() {
      this._effectStore = {}
      this._refsStore = {}
      this._computedStore = {}
    },
    render(h) {
      callIndex = 0
      currentInstance = this
      isMounting = !this._vnode
      const ret = render(h, this.$attrs, this.$props)
      currentInstance = null
      return ret
    }
  }
}
Copy the code

In the code:

WithHooks adds a private local state _state to the component that stores the state values associated with useState and useData.

In Created, the component is injected with some storage-class objects that are needed to support Hooks (useEffect, useRef, useComputed).

The focus is on the render function in the code:

  • CallIndex, which provides a key for a storage object associated with Hooks. This is reset to 0 each time it renders so that it can match the corresponding Hooks based on the call order. This also limits Hooks to being called in top-level code.
  • CurrentInstance, in conjunction with the ensureCurrentInstance function, is used to ensure that Hooks can only be used in functional components.
  • IsMounting Indicates the mounting status of components

useState

UseState is used to add a responsive local state to the component, along with updates associated with that state.

The method signature is:

const [state, setState] = useState(initialState);

SetState is used to update the status:

setState(newState);

UseState Example:

import { withHooks, useState } from "vue-hooks"
const Foo = withHooks(h= > {
  const [count, setCount] = useState(0)
  return h("div", [
    h("span".`count is: ${count}`),
    h("button", { on: { click: (a)= > setCount(count + 1)}},"+")])})Copy the code

In the code, we add a local state count to the component via useState and a function called setCount to update the state value.

UseState implementation details:

export function useState(initial) {
  ensureCurrentInstance()
  const id = ++callIndex
  // Get the local state of the component instance.
  const state = currentInstance.$data._state
  // Local status update, with the increment of id as the key value, stored in the local status.
  const updater = newValue= > {
    state[id] = newValue
  }
  if (isMounting) {
    // Use $set to ensure that the state is responsive.
    currentInstance.$set(state, id, initial)
  }
  // Returns a reactive status update.
  return [state[id], updater]
}
Copy the code

The above code makes it clear that useState creates a local reactive state in the component and generates a status updater.

Note that:

  • The ensureCurrentInstance function ensures that useState must be executed in render, which limits execution to functional components.
  • The increment ID generated by callIndex is used as the key to store the status value. UseState needs to rely on the first render call order to match the past state (callIndex is reset to 0 for each render). This also limits useState to be used in top-level code.
  • Other hooks must follow these two points as well.

useEffect

UseEffect is used to add side effect logic that needs to be executed after component status updates.

Method signature:

void useEffect(rawEffect, deps)

UseEffect specifies the side effect logic that is executed once after the component is mounted, selectively after each component rendering based on the specified dependencies, and when the component is unmounted (if specified), the cleanup logic is performed.

Call Example 1:

import { withHooks, useState, useEffect } from "vue-hooks"

const Foo = withHooks(h= > {
  const [count, setCount] = useState(0)
  useEffect((a)= > {
    document.title = "count is " + count
  })
  return h("div", [
    h("span".`count is: ${count}`),
    h("button", { on: { click: (a)= > setCount(count + 1)}},"+")])})Copy the code

In the code, useEffect resets document.title every time the state value of count changes.

Note: The second argument to useEffect, deps, is not specified here, which means that the logic specified by useEffect will be executed whenever the component is re-rendered, regardless of when count changes.

UseEffect For details, see Using the Effect Hook

Call Example 2:

import { withHooks, useState, useEffect } from "vue-hooks"

const Foo = withHooks(h= > {
  const [width, setWidth] = useState(window.innerWidth)
  const handleResize = (a)= > {
    setWidth(window.innerWidth)
  };
  useEffect((a)= > {
    window.addEventListener("resize", handleResize)
    return (a)= > {
      window.removeEventListener("resize", handleResize)
    }
  }, [])

  return h("div", [
    h("div".`window width is: ${width}`)])})Copy the code

In the code, the useEffect control is used to retrieve the width of the window when it changes.

UseEffect The return value of the first argument, defined as cleanup logic if it is a function. The cleanup logic is performed before the component needs to re-execute the useEffect logic, or when the component is destroyed.

The useEffect logic adds the resize event to the Window object, so the resize event needs to be written off when the component is destroyed or the side effect logic needs to be re-executed to avoid unnecessary event handling.

Note that the second parameter to useEffect is [], indicating that there are no dependencies. The side effect logic is executed only once when the component mounted.

UseEffect implementation details:

export function useEffect(rawEffect, deps) {
  ensureCurrentInstance()
  const id = ++callIndex
  if (isMounting) {
    // Repackage the cleanup logic and side effects logic before mounting the component.
    const cleanup = (a)= > {
      const { current } = cleanup
      if (current) {
        current()
        // Reset to null;
        Cleanup.current is reassigned if the side effect logic is executed twice.
        cleanup.current = null}}const effect = (a)= > {
      const { current } = effect
      if (current) {
        The return value of rawEffect, if a function, is defined as the useEffect side effect cleanup function.
        cleanup.current = current()
        // rawEffect is reset to null;
        // If the associated deps changes, effect.current will be reassigned when rawEffect is executed twice.
        effect.current = null
      }
    }
    effect.current = rawEffect
    // On the component instance, store useEffect related helper members.
    currentInstance._effectStore[id] = {
      effect,
      cleanup,
      deps
    }
    // If the component instance mounts, run the useEffect logic.
    currentInstance.$on('hook:mounted', effect)
    UseEffect cleanup logic is executed when the component instance destroyed.
    currentInstance.$on('hook:destroyed', cleanup)
    // If no dependency is specified or an explicit dependency exists, the component instance updated, and the useEffect logic is executed.
    // If the dependency is [], useEffect is executed only once when mounted.
    if(! deps || deps.length >0) {
      currentInstance.$on('hook:updated', effect)
    }
  } else {
    const record = currentInstance._effectStore[id]
    const { effect, cleanup, deps: prevDeps = [] } = record
    record.deps = deps
    if(! deps || deps.some((d, i) = >d ! == prevDeps[i])) {// If the dependent state value changes, the cleanup logic is executed before the side effect is re-executed.
      cleanup()
      // useEffect sets current to null. Here I reassign effect.current,
      // To execute rawEffect logic after updated.
      effect.current = rawEffect
    }
  }
}
Copy the code

It can be seen that useEffect implementation is exquisite, involving three life cycles of the component: Mounted, updated and destroyed. The execution details of the side effect logic are controlled by the parameter DEPS:

  • In Mounted mode, perform this operation for a fixed time.
  • If DEPS is not specified, each updated is executed.
  • If deps is an empty array, the updated is not executed.
  • If dePS specifies a dependency, it is executed once when the value of the corresponding dependency changes.

With parameters, we can specify three kinds of information for useEffect:

  • RawEffect – Side effect logic content.
  • Cleanup logic – defined by the return value of rawEffect.
  • Dependencies – Defines when side effect logic needs to be repeated.

Where the cleanup logic is executed in two cases:

  • RawEffect needs to be repeated before cleaning up any side effects from the last run.
  • When the component is destroyed.

useRef

This is equivalent to adding a local variable (non-component state) to the component.

Method signature:

const refContainer = useRef(initialValue)

UseRef implementation details:

export function useRef(initial) {
  ensureCurrentInstance()
  const id = ++callIndex
  const { _refsStore: refs } = currentInstance
  return isMounting ?
    (refs[id] = {
      current: initial
    }) :
    refs[id]
}
Copy the code

In the code, the initial value specified by useRef, along with the component’s own REFs definition, is stored in the internal object _refsStore. In the component’s rendering function, you can grab the Ref object: refContainer at any time and get or modify its current property.

useData

UseData is similar to useState except that useData does not provide an updater.

UseData implementation details:

export function useData(initial) {
  const id = ++callIndex
  const state = currentInstance.$data._state
  if (isMounting) {
    currentInstance.$set(state, id, initial)
  }
  return state[id]
}
Copy the code

useMounted

Add logic to be executed in mounted events.

UseMounted implementation details:

export function useMounted(fn) {
  useEffect(fn, [])
}
Copy the code

If useEffect deps is specified as an empty array, fn does not update — it is only executed when mounted.

useDestroyed

Add logic that needs to be executed in the Destroyed phase.

UseDestroyed implementation details:

export function useDestroyed(fn) {
  useEffect((a)= > fn, [])
}
Copy the code

The return value of the useEffect first argument, as mentioned above, is executed as the cleanup logic in the Destroyed phase if it is a function.

Here, fn is prevented from being executed during component updates by setting the value of parameter deps to an empty array and specifying fn as the return value of useEffect side effect logic, making FN executed only in the destroyed phase.

useUpdated

Add logic that needs to be executed after the component is updated.

UseUpdated Implementation details:

export function useUpdated(fn, deps) {
  const isMount = useRef(true)  // Generate an identifier with useRef.
  useEffect((a)= > {
    if (isMount.current) {
      isMount.current = false / / skip mounted.
    } else {
      return fn()
    }
  }, deps)
}
Copy the code

UseEffect is also implemented by using useRef to declare a flag variable to prevent useEffect side effects from being executed in Mounted.

useWatch

Add a Watch to the component.

UseWatch implementation details:

export function useWatch(getter, cb, options) {
  ensureCurrentInstance()
  if (isMounting) {
    currentInstance.$watch(getter, cb, options)
  }
}
Copy the code

This is implemented directly through the component instance’s $watch method.

useComputed

Add computed properties to the component.

UseComputed implementation details:

export function useComputed(getter) {
  ensureCurrentInstance()
  const id = ++callIndex
  const store = currentInstance._computedStore
  if (isMounting) {
    store[id] = getter()
    currentInstance.$watch(
      getter,
      val => { store[id] = val },
      { sync: true})}return store[id]
}
Copy the code

The value of the calculated attribute is stored in the internal object _computedStore. Essentially, this is also implemented through the component instance’s $watch.

Complete code and examples

See also: POC of VUe-hooks

conclusion

Now that you are familiar with the background of Hooks, their definitions, and their implementation in React/Vue, you can basically conclude that:

  • Hooks, if widely used, could dramatically change the way we develop components.
  • Hooks allow us to tap into various parts of the component life cycle to assemble states and behaviors for functional, pure components. Modular granularity is finer, code is more reusable, and code is more cohesive and loose-coupled.
  • Hooks API is a neutral concept and can also be used in Vue, or other component systems such as React’s Hooks API Implemented for Web Components
  • By developing components as “pure component + Hooks”, we basically got rid of the elusive this and the code is more functional. In the future, it will be convenient to further use the advantages of functional curritization, composition, lazy calculation, etc., to write more concise and robust code.
  • Hooks enable us to organize code modules according to the dependencies of business logic, free from the limitations of class component formats.
  • Hooks are in the early stages, but give us a good idea of how to develop components, which you can try out in React-16.7.0-alpha. 0.

The original link: tech.meicai.cn/detail/82, you can also search the small program “Mei CAI product technical team” on wechat, full of dry goods and updated every week, want to learn technology you do not miss oh.