This title is big, but the landing point is very small, just me, a developer, a little feeling and summary in learning and using hooks.

The origin of React hook

The origin of React hook can actually be seen as the result of the continuous evolution of front-end technology.

In the prehistoric era when the world wide web was just born, there was no js, ​​and the web pages were all static, and there were no so-called front-end engineers. The content and updates of the pages were completely generated by the back-end. This makes any update of the page, the page must be refreshed and regenerated by the backend, and the experience is very bad. Then there was Brendan’s ten-day genesis, the Netscape and Microsoft browser disputes, the improvement of HTML, the establishment of the W3C group, and so on.

Later, ajax technology was gradually paid attention to, and the page button submitted/obtained information finally no longer needed to refresh the page, and the interactive experience was upgraded. Then came the era of jquery, which can easily manipulate DOM and achieve various effects, greatly reducing the front-end threshold. The growth of the front-end team has also spawned more and more complex interactions, and the Web page has gradually evolved towards the direction of the Web App.

jquery can insert a large string of HTML structures into the page through $.html, $.append, and $.before. Although it can help us operate the DOM in a more comfortable way, it cannot fundamentally solve the problem. The problem of high pressure on the front end when the amount of DOM operations is too large.

As the page content and interaction become more and more complex, how to solve the problem of front-end side pressure caused by these cumbersome and huge DOM operations, and be able to disperse each HTML into different files, and then render the corresponding one according to the actual situation What about the content?

At this time, the front-ends borrowed from the back-end technology and summed up a formula: html = template(data), which also brought a template engine solution. The template engine solution tends to solve cumbersome DOM manipulation problems point-to-point. It does not and does not intend to replace jquery, and the two coexist.

Subsequently, many template engines were born one after another, such as handlebars, Mustache and so on. No matter which template engine is selected, it is inseparable from the mode of html = template(data). The essence of the template engine is to simplify the process of splicing strings, and quickly build various page structures through HTML-like syntax. Change the data source data to render different effects for the same template.

This makes the template engine, but it also limits it. It is based on “realizing efficient string concatenation”, but it is also limited to this. You can’t expect the template engine to do too complicated things. The performance of the early template engine was not satisfactory. Because it was not smart enough, it updated the DOM by logging out the already rendered DOM and then re-rendering. If the DOM is frequently manipulated, experience and performance will be problematic.

Although the template engine has its limitations, the mode of html=template(data) is still very inspiring. A group of front-end pioneers may have drawn inspiration from it and decided to continue to explore in the direction of “data-driven view”. The problem with the template engine is that the modification of the real DOM is too rough, resulting in a large range of DOM operations, which affects performance.

Since the performance of the real DOM is too expensive, it is better to operate the fake DOM. Since the scope of modification is too large, the scope of each modification becomes smaller.

Therefore, in the template engine solution, the virtual DOM layer is added to the process of “data + template” forming the real DOM.

Note that in the above picture, the “template” on the right is in quotation marks, because it is not necessarily a real template here, and it can act like a template. For example, JSX is not a template, but a js extension similar to template syntax, which has full js capabilities. After adding the virtual DOM layer, there are many things that can be done. First of all, it can be diffed, and the same set of code can be cross-platform.

Now that there is a fake DOM, the DOM can be modified in a more precise way through the coordination process of diff (find differences) + patch (make consistent). We’ve come to the 15.x version of React.

We have class components and functional components at this time, but why do we need to add hooks?

The official statement is this:

  • Difficult to reuse stateful logic between components
  • Complex components become difficult to understand
  • incomprehensible class

It is true that these are the pain points of class, what problems we will encounter when using class to write a simple component:

  • elusive this
  • Associated logic is split
  • Proficiency in memorizing numerous life cycles, and doing appropriate things in the appropriate life cycle
  • The amount of code is relatively large, especially when writing simple components
class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this); // 要手动绑定this
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus( // 订阅和取消订阅逻辑的分散
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() { // 要熟练记忆并使用各种生命周期,在适当的生命周期里做适当的事情
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

The idea of ​​React is expressed in UI = f(data), and the positioning of React components is more like a function. Like the above example, it is actually UI = render(state), the key is the render method, and the rest are parameters passed through this and some peripheral support functions.

In response to the above problems, React decided to bring new capabilities to functional components.

In the past, functional components were stateless components, and they could only passively receive data from the outside. I hope that the component will still retain the value I entered last time or the state of whether it is selected or not after the update. These values ​​​​or states will eventually be reflected on the host instance (DOM for the browser) and displayed to the user. This is too common. demanded. In order to realize this function of maintaining its own local state, React binds a local state to the function component.

This way of binding local state is React hook useState.

In order to allow functions to better build components, React also uses many features to enhance functions, and binding local state is one of the features of these enhanced functions.

The so-called React hooks are those hooks that enhance the capabilities of function components and “hook” these features into pure functions. Pure function components can obtain the ability to bind local states through useState, obtain the ability to perform side effects after page updates through useEffect, and even obtain a complete set of addition, subtraction, value setting, and counter reset through your custom hook useCounter Logic to manage the counter. These are the capabilities given by hooks.

React officially said that there is no plan to remove Class from React, but now the focus is on enhancing functional components. As developers, as long as we are still using React, we cannot completely reject hooks.

Although hooks is not perfect, there are many people complaining about it, let’s try to embrace it.

Implementation of React hooks

As we mentioned earlier, React hooks are a series of features that are beneficial to building UIs and are used to enhance functional components. More specifically, a hook is a special class of functions. So where are these special functions that augment ordinary stateless components? Since we want to explore hooks, we cannot avoid its realization principle.

Let’s take useState as an example. How does it keep the past state when the function component is executed again and again?

Although the source code is complex, the principle may be simple. Simply put, the reason why the state can be kept,It saves an “object” outside of a function component, and the previous state is recorded in this object.

How exactly? Let’s take a look at the implementation of a simplified version of useState.

First of all, we use useState like this:

function App () {
  const [num, setNum] = useState(0);
  const [age, setAge] = useState(18);
  const clickNum = () => {
    setNum(num =>  num + 1);
    // setNum(num =>  num + 1);  // 是可能调用多次的
  }
  const clickAage = () => {
    setNum(age =>  age + 3);
    // setNum(num =>  num + 1);  // 是可能调用多次的
  }
  return <div>
    <button onClick={clickNum}>num: {num}</button>
    <button onClick={clickAage}>age:{age}</button>
  </div>
}

Because jsx needs the support of babel, our abbreviated demo uses useState for UI-independent and simpler display, and we slightly transform the above common usage methods into:

When App() is executed, an object will be returned, and we call the method of the object to simulate a click.

function App () {
    const [num, setNum] = useState(0);
    const [age, setAge] = useState(10);
    console.log(isMount ? '初次渲染' : '更新');
    console.log('num:', num);
    console.log('age:', age);
    const clickNum = () => {
      setNum(num =>  num + 1);
    //   setNum(num =>  num + 1);  // 是可能调用多次的
    }
    const clickAge = () => {
      setAge(age =>  age + 3);
      // setNum(num =>  num + 1);  // 是可能调用多次的
    }
    return {
      clickNum,
      clickAge
    }
  }

So let’s start with this function.

First of all, the component must be mounted on the page, and the App function must be executed. Right at the beginning of execution, we encounter the useState function. Now forget for a moment that useState is a React hook, it’s just a function, like any other.

Before starting the useState function, let’s briefly understand the data structure of the linked list.

u1 -> u2 -> u3 -> u1, this is a circular linked list.

u1 -> u2 -> u3 -> null, this is a one-way linked list.

The linked list we use requires only these preliminaries, which can ensure that we can read data conveniently in a certain order.

In the previously stated useState principle, we mentioned:

The reason why the state can be kept is that an “object” is saved outside of a function component, and the previous state is recorded in this object.

Then we declare some necessary things outside the function:

// 组件是分初次渲染和后续更新的,那么就需要一个东西来判断这两个不同阶段,简单起见,我们是使用这个变量好了。
let isMount = true;  // 最开始肯定是true

// 我们在组件中,经常是使用多个useState的,那么需要一个变量,来记录我们当前实在处理那个hook。
let workInProgressHook = null; // 指向当前正在处理的那个hook

// 针对App这个组件,我们需要一种数据结构来记录App内所使用的hook都有哪些,以及记录App函数本身。这种结构我们就命名为fiber
const fiber = {
  stateNode: App, // 对函组件来说,stateNode就是函数本身
  memorizedState: null // 链表结构。用来记录App里所使用的hook的。
}

// 使用 setNum是会更新组件的, 那么我们也需要一种可以更新组件的方法。这个方法就叫做 schedule
function schedule () {
  // 每次执行更新组件时,都需要从头开始执行各个useState,而fiber.memorizedState记录着链表的起点。即workInProgressHook重置为hook链表的起点
  workInProgressHook = fiber.memorizedState;
  // 执行 App()
  const app = fiber.stateNode(); 
  // 执行完 App函数了,意味着初次渲染已经结束了,这时候标志位该改变了。
  isMount = false;
  return app;
}

The outside things are ready, start the inside of the useState function.

Before we start, we have a few questions about useState:

  • How does useState keep the previous state?

  • If a function that updates the state such as setNum is called multiple times, what should be done with these functions?

  • If this useState is executed, how do you know where to find the next hook?

With these questions in mind, we enter the interior of useState:

// 计算新状态,返回改变状态的方法
function useState(initialState) {
    // 声明一个hook对象,hook对象里将有三个属性,分别用来记录一些东西,这些东西跟我们上述的三个疑问相关
    // 1. memorizedState, 记录着state的初始状态 (疑问1相关)
    // 2. queue, queue.pending 也是个链表,像上面所说,setNum是可能被调用多次的,这里的链表,就是记录这些setNum。 (疑问2相关)
    // 3. next, 链表结构,表示在App函数中所使用的下一个useState (疑问3相关)
      let hook; 
    if (isMount) {
      // 首次渲染,也就是第一次进入到本useState内部,每一个useState对应一个自己的hook对象,所以这时候本useState还没有自己的的hook数据结构,创建一个
      hook = {
        memorizedState: initialState,
        queue: {
          pending: null // 此时还是null的,当我们以后调用setNum时,这里才会被改变
        },
        next: null
      }
      // 虽然现在是在首次渲染阶段,但是,却不一定是进入的第一个useState,需要判断
      if (!fiber.memorizedState) {
        // 这时候才是首次渲染的第一个useState. 将当前hook赋值给fiber.memorizedState
        fiber.memorizedState = hook; 
      } else {
        // 首次渲染进入的第2、3、4...N 个useState
        // 前面我们提到过,workInProgressHook的用处是,记录当前正在处理的hook (即useState),当进入第N(N>1)个useState时,workInProgressHook已经存在了,并且指向了上一个hook
        // 这时候我们需要把本hook,添加到这个链表的结尾
        workInProgressHook.next = hook;
      }
      // workInProgressHook指向当前的hook
      workInProgressHook = hook;
    } else {
      // 非首次渲染的更新阶段
      // 只要不是首次渲染,workInProgressHook所在的这条记录hook顺序的链表肯定已经建立好了。而且 fiber.memorizedState 记录着这条链表的起点。
      // 组件更新,也就是至少经历了一次schedule方法,在schedule方法里,有两个步骤:
      // 1. workInProgressHook = fiber.memorizedState,将workInProgressHook置为hook链表的起点。初次渲染阶段建立好了hook链表,所以更新时,workInProgressHook肯定是存在的
      // 2. 执行App函数,意味着App函数里所有的hook也会被重新执行一遍
      hook = workInProgressHook; // 更新阶段此时的hook,是初次渲染时已经建立好的hook,取出来即可。 所以,这就是为什么不能在条件语句中使用React hook。
      // 将workInProgressHook往后移动一位,下次进来时的workInProgressHook就是下一个当前的hook
      workInProgressHook = workInProgressHook.next;
    }
    // 上述都是在建立、操作hook链表,useState还要处理state。
    let state = hook.memorizedState; // 可能是传参的初始值,也可能是记录的上一个状态值。新的状态,都是在上一个状态的基础上处理的。
    if (hook.queue.pending) {
      let firstUpdate = hook.queue.pending.next; // hook.queue.pending是个环装链表,记录着多次调用setNum的顺序,并且指向着链表的最后一个,那么hook.queue.pending.next就指向了第一个
      do {
        const action = firstUpdate.action;
        state = action(state); // 所以,多次调用setNum,state是这么被计算出来的
        firstUpdate.next = firstUpdate.next
      } while (firstUpdate !== hook.queue.pending.next) // 一直处理action,直到回到环状链表第一位,说明已经完全处理了
      hook.queue.pending = null;
    }
    hook.memorizedState = state; // 这就是useState能保持住过去的state的原因
    return [state, dispatchAction.bind(null, hook.queue)]
  }

In useState, two things are mainly done:

  • Create a linked list of hooks. Link all used hooks together in order, and move the pointer so that the hooks recorded in the linked list and the currently processed hooks can correspond one-to-one.

  • Process state. On the basis of the previous state, the action function is continuously called through the hook.queue.pending linked list until the latest state is calculated.

At the end, diapatchAction.bind(null, hook.queue) is returned, which is the real body of setNum. It can be seen that in the setNum function, hook.queue is hidden.

Next, let’s look at the implementation of dispatchAction.

function dispatchAction(queue, action) {
    // 每次dispatchAction触发的更新,都是用一个update对象来表述
    const update = {
      action,
      next: null // 记录多次调用该dispatchAction的顺序的链表
    }
    if (queue.pending === null) {
      // 说明此时,是这个hook的第一次调用dispatchAction
      // 建立一个环状链表
      update.next = update;
    } else {
      // 非第一调用dispatchAction
      // 将当前的update的下一个update指向queue.pending.next 
      update.next = queue.pending.next;        
      // 将当前update添加到queue.pending链表的最后一位
      queue.pending.next = update;
      }
    queue.pending = update; // 把每次dispatchAction 都把update赋值给queue.pending, queue.pending会在下一次dispatchAction中被使用,用来代表上一个update,从而建立起链表
    // 每次dispatchAction都触发更新
    schedule();
  }
  

In the above code, lines 7-18 are not easy to understand, so let me explain briefly.

Suppose we call the setNum function 3 times and generate 3 updates, A, B, and C.

When the first update A is generated:

A: At this time queue.pending === null,

Execute update.next = update, ie A.next = A;

then queue.pending = A;

Create a circular linked list of A -> A

B: At this point queue.pending already exists,

update.next = queue.pending.next ie B.next = A.next ie B.next = A

queue.pending.next = update; That is, A.next = B, breaking the chain of A->A, A->B

queue.pending = update ie queue.pending = B

Create a circular linked list of B -> A -> B

C: queue.pending already exists at this time

update.next = queue.pending.next, ie C.next = B.next, and B.next = A, C.next = A

queue.pending.next = update, ie B.next = C

queue.pending = update, that is queue.pending = C

Since C -> A, B -> C, and in the second step, A points to B, that is

Create a C -> A -> B -> C circular linked list

Now, we have completed the simple useState code, you can try it out, the whole code is as follows:

let isMount = true;
let workInProgressHook = null;
const fiber = {
  stateNode: App,
  memorizedState: null
}

function schedule () {
  workInProgressHook = fiber.memorizedState;
  const app = fiber.stateNode(); 
  isMount = false;
  return app;
}

function useState(initialState) {
      let hook; 
    if (isMount) {
      hook = {
        memorizedState: initialState,
        queue: {
          pending: null
        },
        next: null
      }
      if (!fiber.memorizedState) {
        fiber.memorizedState = hook; 
      } else {
        workInProgressHook.next = hook;
      }
      workInProgressHook = hook;
    } else {
      hook = workInProgressHook;
      workInProgressHook = workInProgressHook.next;
    }
    let state = hook.memorizedState;
    if (hook.queue.pending) {
        let firstUpdate = hook.queue.pending.next
        do {
            const action = firstUpdate.action;
            state = action(state);
            firstUpdate.next = firstUpdate.next
        } while (firstUpdate !== hook.queue.pending.next)
      hook.queue.pending = null;
    }
    hook.memorizedState = state;
    return [state, dispatchAction.bind(null, hook.queue)]
  }

  function dispatchAction(queue, action) {
    const update = {
      action,
      next: null
    }
    if (queue.pending === null) {
      update.next = update;
    } else {
      update.next = queue.pending.next;        
      queue.pending.next = update;
      }
    queue.pending = update;
    schedule();
  }

  function App () {
    const [num, setNum] = useState(0);
    const [age, setAge] = useState(10);
    console.log(isMount ? '初次渲染' : '更新');
    console.log('num:', num);
    console.log('age:', age);
    const clickNum = () => {
      setNum(num =>  num + 1);
    //   setNum(num =>  num + 1);  // 是可能调用多次的
    }
    const clickAge = () => {
      setAge(age =>  age + 3);
      // setNum(num =>  num + 1);  // 是可能调用多次的
    }
    return {
      clickNum,
      clickAge
    }
  }

  window.App = schedule();

Copy and paste in browser console, try App.clickNum() , App.clickAge() .

Since we call schedule every update, hook.queue.pending will be executed as long as it exists, and then hook.queue.pending = null, so in our simplified version of useState, the ring created by queue.pending The linked list is not used. In real React, batchedUpdates will trigger an update after multiple dispatchActions are executed. At this time, a circular linked list is needed.

I believe that through the detailed code comments above, we should have the answer to the three questions we asked about useState earlier.

– How does useState keep the previous state?

Each hook records the last state, and then recalculates according to the action saved in the queue.pending linked list, and returns the new state. And record the state at this time for the next state update.

– What happens to dispatch functions like setNum if I call them multiple times?

Multiple calls to the dispatchAction function will be stored in hook.queue.pending as an update basis. Every time the component is updated, hook.queue.pending is set to null. If there is a dispatchAction later, it will continue to be added to hook.queue.pending, and the action will be executed sequentially in the useState function, and then set to null again.

– If this useState is executed, where should the next hook be found?

When rendering for the first time, all hooks used in the component exist in the fiber.memorizedState corresponding to the component in the form of a linked list in order, and a workInProgress is used to mark the hook currently being processed. After each one is processed, workInProgress will move to the next hook, ensuring that the order of the processed hooks strictly corresponds to the order of collection during the first rendering.

The concept of React hooks

According to Dan’s blog (https://overreacted.io/zh-hans/algebraic-effects-for-the-rest-of-us/), React hook is practicing algebraic effects. I don’t know much about algebraic effects, so I can only vaguely understand “algebraic effects” as “using a certain method (expression/grammar) to obtain a certain effect”, just like using useState to let components get state, For users, there is no need to care about how it is implemented, React will handle it for us, and we can focus on what to do with these effects.

But why did the hook choose functional components? What makes functional and class components so different?

In terms of writing, the writing of class components in the past is basically imperative. When a certain condition is met, do something.

class Box extends React.components {
  componentDidMount () {
    // fetch data
  }
  componentWillReceiveProps (props, nextProps) {
    if (nextProps.id !== props.id) {
      // this.setState
    }
  }
}

However, the writing method of hook has become declarative. First, some dependencies are declared, and when the dependencies change, some logic is automatically executed.

function Box () {
  useEffect(() => {
    // fetch data
  }, [])
  useEffect(() => {
    // setState
  }, [id])
}

Which of the two is better may vary from person to person. But I think funciton is definitely more approachable for people who are getting into React for the first time.

The biggest difference between the two is the update method. The class component changes the props and state values ​​in this of the component instance, and then re-executes render to get the latest props and state. The component will not be instantiated again .

Functional components, on the other hand, re-execute the function itself when updated. Each function executed can be regarded as independent of each other.

I think the difference in this update method is the main reason why I am not used to functional components. Function components capture the values ​​needed for rendering, so in some cases the results are always unexpected.

Like this example (https://codesandbox.io/s/react-hooks-playground-forked-ktf4uw?file=/src/index.tsx), when we click on the plus sign and try to drag the window, see the control What did the station print?

yes,count is 0. Although you have clicked the button many times, even though the latest count has become N, the context of each App is independent. When handleWindowResize is defined, the count it sees is 0, and then it is bound to the event. Since then, the change of count has nothing to do with the handleWindowResize in this App world. There will be a new handleWindowResize to see the new count, but only the original one is bound to the event.

And the Class version is like this:

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
    this.handleWindowResize = this.handleWindowResize.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }
  handleWindowResize() {
    console.log(`count is ${this.state.count}`);
  }
  handleClick() {
    this.setState({
      count: this.state.count + 1
    });
  }
  componentDidMount() {
    window.addEventListener("resize", this.handleWindowResize);
  }
  componentWillUnmount () {
    window.removeEventListener('resize', this.handleWindowResize)
  }
  render() {
    const { count } = this.state;
    return (
      <div className="App">
        <button onClick={this.handleClick}>+</button>
        <h1>{count}</h1>
      </div>
    );
  }
}

In the version of Class, the instance of App will not be created again, no matter how many times this.handleWindowResize and this.handleClick are updated, it is still the same, but the this.state inside is modified by React.

From this example, we can somewhat see the meaning of “the function captures the value required for rendering”, which is also the main difference between function components and class components.

But why add hooks to functional components?

Adding hooks to functional components, in addition to solving several defects of class:

In addition, from the perspective of the React concept, UI = f(state) also shows that components should only be channels for data, and components are essentially closer to functions. From the perspective of personal use, in fact, in many cases, we don’t need such a heavy class, but we just suffer from the fact that stateless components cannot setState.

The meaning of React hook

So many hooks mentioned above are all about how it is implemented and how it is different from Class components, but there is still one most fundamental question that has not been answered.What does hook bring to us developers?

As mentioned in the official website of React, Hook can make the state available logic between components simpler, and compare it with high-level components, but the first question is, why do we use HOC?

Look at this example first (https://codesandbox.io/s/modest-visvesvaraya-v0i9fy?file=/src/Counter.jsx):

import React from "react";

function Count({ count, add, minus }) {
  return (
    <div style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
      <div>You clicked {count} times</div>
      <button
        onClick={add}
        title={"add"}
        style={{ minHeight: 20, minWidth: 100 }}
      >
        +1
      </button>
      <button
        onClick={minus}
        title={"minus"}
        style={{ minHeight: 20, minWidth: 100 }}
      >
        -1
      </button>
    </div>
  );
}

const countNumber = (initNumber) => (WrappedComponent) =>
  class CountNumber extends React.Component {
    state = { count: initNumber };
    add = () => this.setState({ count: this.state.count + 1 });
    minus = () => this.setState({ count: this.state.count - 1 });
    render() {
      return (
        <WrappedComponent
          {...this.props}
          count={this.state.count}
          add={this.add.bind(this)}
          minus={this.minus.bind(this)}
        />
      );
    }
  };
export default countNumber(0)(Count);

The effect is to display the current value, click to produce addition and subtraction effects.

The reason for using this method is to provide a set of reusable states and methods outside the wrapped component. In this example, they are state, add, and minus, so that if there are other WrappedComponents with similar functions but different styles later, they can be directly wrapped with countNumber.

From this example, we can actually see that we use HOC to do two things essentially,Pass by value and sync.

Passing by value is to pass the value obtained outside to our wrapped component. Synchronization, on the other hand, allows the wrapped component to re-render with the new value.

From here we can roughly see two disadvantages of HOC:

  • Because we have limited ways to re-render subcomponents, either high-level component setState, or forceUpdate, and these methods are all within React components and cannot be used independently of React components, so the business logic and display of add\minus UI logic had to be glued together.

  • When using HOC, we often use multiple HOC nested. However, HOC follows the convention of transparently transmitting props that have nothing to do with itself, resulting in too many props that are not very related to the component when it finally reaches our component, and debugging is quite complicated. We don’t have a good way to get around the hassle of having multiple layers of HOC nesting.

Based on these two points, we can say that hooks brings the so-called “elegant” way to solve the problems of logic reuse and nesting hell better than HOC.

The business logic in the HOC cannot be extracted to a function outside the component. Only within the component can there be a way to trigger the re-rendering of the sub-component. Now that custom hooks bring the ability to trigger component re-rendering outside the component, the problem is solved.

Use hook, no need to mix business logic and UI components together, the above example (https://codesandbox.io/s/modest-visvesvaraya-v0i9fy?file=/src/CounterWithHook.jsx), use hook The way it is implemented is this way:


// 业务逻辑拆分到这里了
import { useState } from "react";

function useCounter() {
  const [count, setCount] = useState(0);
  const add = () => setCount((count) => count + 1);
  const minus = () => setCount((count) => count - 1);
  return {
    count,
    add,
    minus
  };
}
export default useCounter;
// 纯UI展示组件
import React from "react";
import useCounter from "./counterHook";

function Count() {
  const { count, add, minus } = useCounter();
  return (
    <div style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
      <div>You clicked {count} times</div>
      <button
        onClick={add}
        title={"add"}
        style={{ minHeight: 20, minWidth: 100 }}
      >
        +1
      </button>
      <button
        onClick={minus}
        title={"minus"}
        style={{ minHeight: 20, minWidth: 100 }}
      >
        -1
      </button>
    </div>
  );
}
export default Count;

This split allows us to finally separate the business from the UI. If you want to obtain the ability similar to the previous nested HOC, you only need to introduce another line of hook.


function Count() {
  const { count, add, minus } = useCounter();
  const { loading } = useLoading();
  return loading ? (
    <div>loading...please wait...</div>
  ) : (
    <div style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
      ...
    </div>
  );
}
export default Count;

useCounter and useLoading maintain their own, and the things we introduced are clear at a glance.

In this counter example, we can think a little further. Now that the logic and UI can be separated, if all the logic of a counter is extracted, can any UI library be applied?

Starting from this assumption, if I let the hook provide these capabilities:

  • You can set the initial value of the counter, each addition and subtraction value, maximum and minimum values, precision

  • You can directly obtain the effect that the button becomes gray and cannot be clicked when the maximum and minimum values ​​are exceeded by returning the method.

  • You can directly obtain the functions of the middle input box that can only input numbers, but not text, etc. by returning the method.

And what the developer has to do is to put these on top of any UI library or native buttons and inputs, that’s all.


function HookUsage() {
  const { getInputProps, getIncrementButtonProps, getDecrementButtonProps } =
    useNumberInput({
      step: 0.01,
      defaultValue: 1.53,
      min: 1,
      max: 6,
      precision: 2,
    })

  const inc = getIncrementButtonProps()
  const dec = getDecrementButtonProps()
  const input = getInputProps()

  return (
    <HStack maxW='320px'>
      <Button {...inc}>+</Button>
      <Input {...input} />
      <Button {...dec}>-</Button>
    </HStack>
  )
}

It only takes a few lines of code to get this effect:

We can look forward to a completely different component library from Antd and elementUI. It can only provide refined business component logic without providing UI, and you can apply these business logics to any UI library.

The emergence of Hooks can turn UI library into logic library, and this design concept of separating component state, logic and UI display concerns is also called Headless UI.

Headless UI focuses on providing the interaction logic of components, and the UI part allows users to choose freely. On the premise of satisfying the customization and expansion of UI, it can also realize the reuse of logical interaction logic. There have been some explorations in this area in the industry, such as Component library chakra. The previous examples are actually the capabilities provided by chakra. The bottom layer of chakra is the practice of Headless UI, providing external hooks, and the upper layer provides components with built-in UI. It is impossible to really write styles for buttons and inputs from scratch. In addition, chakra provides atomic components that form a large component, such as the Table component. Chakra will provide:

Table,
  Thead,
  Tbody,
  Tfoot,
  Tr,
  Th,
  Td,
  TableCaption,
  TableContainer,

This allows users to freely combine according to actual needs. Material-UI also provides atomic components. For us who are used to the overall components of Antd and element UI, it provides a different way, which is worth trying .

In actual development, based on hooks, even experienced people can be responsible for writing logic, while novices can write UI, division of labor and cooperation, greatly improving development efficiency.

Limitations of React hooks

The proposal of React hook is of great significance in the front end, but there is no perfect thing in the world, and there are still some problems in hook. Of course, this is just a little feeling and confusion in my personal use.

forced order

When I first came across hooks, I was surprised that you couldn’t use hooks in nests or conditions, which was very counter-intuitive.

useState and useEffect are obviously just a function, but where can I use them? As long as the parameters I pass in are correct, where do you care if I use them?

When we usually write some tool functions in the project, will we restrict where others can use them? At most, it is to judge the host environment, but there is definitely no limit to the order of use. A good function should be like a pure function, the same input brings the same output, no side effects, no need for the user to think about the environment, level, and order of calling, and the least cognitive burden on the user.

Later, I learned that React uses the timing of calls to ensure the correct state within the component.

Of course, this is not a big problem. When jsx first came out, the syntax of js mixed with html was also criticized, and now it is really fragrant. As long as developers remember and abide by these rules, there is no burden.

Complex useEffct

I believe that when many students encounter this API, the first problem is that they are confused by the description of “execution side effects”.

What are execution side effects?

React says data fetching, setting subscriptions, and manually changing the DOM are side effects.

Why are these called side effects?

Because ideally, whether it is a Class component or a function component, it is best to be a pure function without any side effects. The same input will always produce the same output, which is stable, reliable and predictable. But in fact, it is very common to execute some logic after the rendering of the component is completed, just like modifying the DOM and requesting the interface after the rendering of the component. So in order to meet the requirements, although the render in the Class component is a pure function, the side effects of the Class component are placed in the componentDidMount life cycle. Even though the formerly stateless components were pure functions, useEffect was added to do something after the page renders. The so-called side effect is that componentDidMount makes the render of Class impure. useEffect makes the stateless function component impure.

Even if we understand the side effects, the next step is to understand useEffect itself.

Students who first come into contact with the React life cycle, when learning useEffct, will more or less use componentDidMount to compare useEffct. If you are familiar with Vue, you may think that useEffct is similar to watcher, but when you use it a lot, you will find that useEffect is specious.

First of all, useEffect will be executed after each rendering is completed, just judge whether to execute your effect according to the dependency array. It is not componentDidMount, but in order to achieve the effect of componentDidMount, we need to use an empty array to simulate. At this time useEffect can be regarded as componentDidMount. When the dependent array is empty, the cleanup method returned in effect is equivalent to componentWillUnmount.

In addition to implementing componentDidMount and componentWillUnmount, useEffect can also set variables that need to be monitored in the dependency array, which looks like a Vue watcher. But useEffect is actually executed after the page is updated. for example:


function App () {
  let varibaleCannotReRender; // 普通变量,改变它并不会触发组件重新渲染
  useEffect(() => {
          // some code
        }, [varibaleCannotReRender])
  // 比如在一次点击事件中改变了varibaleCannotReRender
  varibaleCannotReRender="123"
}

The page will not render, the effect will definitely not execute, and there will be no prompt. So useEffect is not a variable watcher either. In fact, as long as the page is re-rendered, even non-props/state local variables in the dependency array of your useEffect can trigger the effect.

Like this, after each click, the effect will be executed, although I don’t monitor num, and b is just a normal variable.


function App() {
  const [num, setNum] = useState(0);
  let b = 1;
  useEffect(() => {
    console.log('effefct', b);
  }, [b]);
  const click = () => {
    b = Math.random();
    set((num) => num + 1);
  };

  return <div onClick={click}>App {get}</div>;
}

Therefore, in understanding useEffect, the experience of the past React life cycle and Vue’s watcher cannot be transferred well. Perhaps the best way is to forget the past experience and learn from scratch.

Of course, even if you already know what effect useEffect can bring in different situations, it doesn’t mean you can use it well. This is especially true for developers who used to use Class components heavily, and it is not enough to get rid of the life cycle.

In the Class component, UI rendering is determined by props or state in the render function, and render can be a pure function without side effects. Every time this.setState is called, new props and state render a new UI, UI and props, Consistency between states is maintained. The side effects in componentDidMount do not participate in the update process, and lose synchronization with the update. This is the thinking mode of Class. In the useEffect mode of thinking, useEffect is synchronized with each update, there is no mount and update here, and the first rendering and the tenth rendering are treated equally. Every render can execute your effect if you want.

This difference in mindset is what I think makes useEffect confusing.

useEffect is synchronized with the update, but in actual business, it is not necessary to execute the effect for every update, so you need to use the dependency array to decide when to execute the side effect and when not to execute it. Improper filling of the dependent array may cause infinite execution of effects, or outdated values ​​in effects, etc.

When writing business, we always involuntarily write more and more functions and components, and useEffect becomes more and more complicated. When the dependent array becomes longer and longer, it is time to consider whether there is a design problem. We should try our best to follow the principle of singleness, and let each useEffect do only one thing as simple as possible. Of course, this is not easy.

functional purity

Before the appearance of hooks, functional components had no local state, and the information was passed in from the outside. It was like a pure function. Once the function was re-executed, the variables and methods you declared in the component were all new, simple and pure . At that time, we still called this type of component SFC (staless function component).

But after the hook is introduced, the stateless function component has the ability of local state and becomes FC. Strictly speaking, a function with local state at this time cannot be regarded as a pure function, but in order to reduce the burden of thinking, useState can be understood as a kind of data declared outside the function component. Every change is passed to your function component.

// 把这种
function YourComponent () {
  const [num, setNum] = useState(0);
  return <span>{num}</span>
}


// 理解成这种形式,使用了useState,React就自动给你生成AutoContainer包裹你的函数。这样你的组件仍可以看成是纯函数。
 function AutoContainer () {
   const [num, setNum] = useState(0);
   return <YourComponent num={num} />
 }
function YourComponent (props) {
  return <span>{props.num}</span>
}

Every function component update captures the current environment like a snapshot, and each update has a unique context. The updates do not interfere with each other, making full use of the closure feature, which seems to be very pure.

If it’s always been like this, it’s okay, each update is like a parallel universe, similar but not the same. The context of props, state, etc. decides to render, and after the rendering is a new context, each passing its own without disturbing each other.

But in fact, the emergence of useRef breaks this purity. useRef allows the component to return the same object every time it is rendered, just like a space gem, shuttling between parallel universes. This feature of useRef makes it the closure savior of hooks, and also creates a situation of “useRef in case of trouble”. It is agreed that each rendering is a unique context, how can we still get the data updated several times ago?

Is it okay to remove useRef? Really not, some scenes just need some values ​​to pass through multiple renderings.

But isn’t this equivalent to adding a field to store data in this of the class component? It can be seen that React wants to embrace functional programming, but useRef makes it less “functional”.

write at the end

It is relatively late for me to actually start using hooks. This long-winded article with more than 10,000 words is actually a question I asked myself during my study and use. Here I try to answer these questions. My ability is limited, and I can only describe some things I understand from some partial perspectives. It is not comprehensive and not deep enough. As I continue to study, I may have different insights in the future.

Hooks are very powerful. Mastering a powerful skill has always required continuous training and time precipitation. I hope this article can answer some of the questions you have ever had.

Reference documents: https://overreacted.io/ https://zh-hans.reactjs.org/docs/thinking-in-react.html https://zh-hans.reactjs.org/docs/hooks-effect. html https://react.iamkasong.com/ https://juejin.cn/post/6944863057000529933

#talking #React #hooks #talking #UPYUNunstructured #data #cloud #storage #cloud #processing #cloud #distribution #platform #News Fast Delivery

Leave a Comment

Your email address will not be published. Required fields are marked *