React Hook was introduced by Dan Abramov at the React Conf in 2018. It is a new way for function components to support states and other React features, and is officially described as the start of the next five years of React evolving. The importance of React Hook can be seen in detail. React Hook was released with React V16.8.0 on February 6, and the React community spent the first half of the year embracing it, learning about it, and interpreting it. Although React Hook is officially in the process of rapid development and update, many features supported by Class Component are not yet supported by React Hook, but this does not affect the community’s enthusiasm for learning.

React Hook is very simple to get started and easy to use, but there are still some conceptual and ideological changes compared to the class component writing method we’ve been familiar with for 5 years. The React team also has some rules that use hooks and the ESLint plugin to help reduce the probability of rule violations, but rules aren’t just for memorizing, it’s important to really understand why and the context in which they were designed.

Much of the code in this article is pseudo-code, focused on interpreting design ideas, and therefore not a complete implementation. A lot of the list building and updating logic is also omitted, but it doesn’t affect the overall React Hook design. In fact, most of React Hook’s code ADAPTS to the React Fiber architecture, which is why the source code is so obscure. It doesn’t matter, though, that we can remove React Fiber and build a pure React Hook architecture.

The background and intention of the design

What problem was React Hook mainly created to solve? The official document is very clear. This is a brief summary, not too much. If you haven’t read the document, you can read the Introduction to React Hook first.

To sum up the pain points to be solved are:

  1. Reusing state logic between components is difficult
    • The previous solution was: Render props and higher-order components.
    • The disadvantage is that it is difficult to understand and there is too much nesting to form “nested hell”.

  1. Complex components become difficult to understand

    • Lifecycle functions are riddled with state logic and side effects.
    • These side effects are hard to reuse and sporadic.
  2. Hard to understand Class

    • This pointer problem.

    • The component precompilation technique (component folding) encounters optimization failures in class cases.

    • Class does not compress well.

    • Class is unstable under thermal loading.

Design scheme

React:

To address these issues, Hook allows you to use more React features when == is not class. Conceptually, the React component has always been more like a function. Hook embraced functions without sacrificing the spirit of React. Hooks provide solutions to problems without learning complex functional or reactive programming techniques

Design goals and principles

React Hook is designed to solve the problems raised in section 1, which can be summarized as follows:

  • No Class complexity

  • No life cycle problems

  • Gracefully reuse

  • Align the capabilities that the React Class component already has

Design scheme

React 16.8 Before React 16.8 was released, there were two main types of components based on whether they had state maintenance:

  1. Class Component: Used mainly for complex components that require internal state and have side effects

    class App extends React.Component{
        constructor(props){
            super(props);
            this.state = {
                //...
            }
        }
        //...
    }
    Copy the code
  2. Function Component: Used primarily for pure components that contain no state and are equivalent to a template Function

    function Footer(links){
        return (
            <footer>
                <ul>
                {links.map(({href, title})=>{
                    return <li><a href={href}>{title}</a></li>
                })}
                </ul>
            </footer>
        )
    }
    Copy the code

If the design goal is to remove Class==, it seems that the only choice is to modify Function Components to have the same capabilities as Class Components.

Let’s think about the final state-supporting function component code:

/ / timerfunction Counter() {let state = {count:0}
    
    function clickHandler() {setState({count: state.count+1})   
    }
    
    return (
        <div>
            <span>{count}</span>
            <button onClick={clickHandler}>increment</button>
        </div>
    )
}
Copy the code

The above code uses the function component to define a Counter component, which provides the state state and the setState function that changes the state. These apis are certainly familiar to the Class Component, but present different challenges in the Function Component:

  1. Class instances can permanently store the state of the instance, whereas functions cannot. State is reassigned to 0 every time Counter is executed.
  1. Each instance of Class Component has a member Function this.setState to change its state. Function Component is a Function and does not have this.setState. The correspondence can only be achieved through the global setState method, or some other method.

The above two problems are the ones to be solved when choosing to transform Function Component.

The solution

There are several ways to store persistent state in JS:

  • Class instance attributes

    class A() {constructor(){
            this.count = 0;
        }
        increment() {return this.count ++;
        }
    }
    const a = new A();
    a.increment();
    Copy the code
  • The global variable

     const global = {count:0};
    
     function increment() {return global.count++;
     }
    Copy the code
  • DOM

      const count = 0;
      const $counter = $('#counter');
      $counter.data('count', count);
      
      funciton increment(){
          const newCount = parseInt($counter.data('count'), 10) + 1;
          $counter.data('count',newCount);
          return newCount;
      }
    Copy the code
  • closure

      const Counter = function() {let count = 0;
          return {
              increment: ()=>{
                  return count ++;
              }
          }
      }()
      
      Counter.increment();
    
    Copy the code
  • Other global stores: indexDB, LocalStorage, etc. Function Component only wants state to be accessible, so all of the above seems feasible. However, as a good design, the following points need to be taken into account:

    • Using a simple

    • Performance and efficient

    • Reliable without side effects

Options 2 and 5 clearly do not meet the third point; Option 3 does not consider either aspect; Closures are therefore the only option.

Implementation of closures

Since it is a closure, there will be a change in usage. Suppose we expect to provide a function called useState that uses the closure to access the component’s state, and a dispatch function that updates the state and assigns an initial value to it through the initial call.

function Counter(){
    const [count, dispatch] = useState(0)
    
    return (
        <div>
            <span>{count}</span>
            <button onClick={dispatch(count+1)}>increment</button>
        </div>
    )
}
Copy the code

If you’ve ever used Redux, this scene looks very familiar. That’s right, isn’t it a miniature version of redux one-way data flow?

Given an initial state, an action is dispatched, the state is changed via the Reducer, and the new state is returned to trigger component re-rendering.

Knowing this, the implementation of useState is clear:

function useState(initialState){
    let state = initialState;
    function dispatch = (newState, action)=>{
        state = newState;
    }
    return [state, dispatch]
}

Copy the code

The code above is straightforward, but it still doesn’t cut it. The Function Component re-executes the useState Function after initialization or after a state change, and ensures that the state is up to date every time useState is executed.

Obviously, we need a new data structure to hold the last state and the current state, so that the initialization process calls useState and the update process calls useState can get the correct corresponding values. The data structure can be designed as follows. Let’s say the data structure is called Hook:

typeHook = {memoizedState: any, / / the last complete update after the final state of the value of the queue: UpdateQueue < any, any > | null, / / update the queue};Copy the code

Considering the difference between the mounting of the first component and the subsequent updating logic, we defined two different implementations of useState functions called mountState and updateState respectively.

function useState(initialState){
    if(isMounting){
        return mountState(initialState);
    }
    
    if(isUpdateing){
        returnupdateState(initialState); }} // The method actually called the first time the component's useState is calledfunction mountState(initialState){
    let hook = createNewHook();
    hook.memoizedState = initalState;
    return [hook.memoizedState, dispatchAction]
}

functionDispatchAction (action){// Use data structures to store all update actions so that the latest state value storeUpdateActions(action) is calculated in rerender; // Execute fiber's scheduleWork(); } // The method actually called each time useState is executed after the first timefunctionUpdateState (initialState){// Calculate the new status value based on the update action stored in dispatchAction and return it to the componentdoReducerWork();
    
    return [hook.memoizedState, dispatchAction];
}   

function createNewHook() {return {
        memoizedState: null,
        baseUpdate: null
    }
}

Copy the code

The above code basically reflects our design approach, but there are two core issues that need to be addressed:

  1. How this update behavior will be shared with doReducerWork after calling storeUpdateActions for the final state calculation.

  2. How to share hook objects when calling mountState and updateState at different times in the same state?

Update logic sharing

Update logic is an abstract description, and we first need to think about what information an update should contain based on how it is actually used. In fact, we can call dispatchAction multiple times within an event handler function:


function Count(){
    const [count, setCount] = useState(0);
    const [countTime, setCountTime] = useState(null);
    
    function clickHandler(){// call dispatchAction multiple timessetCount(1);
        setCount(2);
        setCount(3); / /...setCountTime(Date.now())
    }
    
    return (
    <div>
        <div>{count} in {countTime}</div>
        <button onClick={clickHandler} >update counter</button>
    </div>
    )
}

Copy the code

In the three calls to setCount, we do not want the Count component to be rendered three times, but rather to implement the state of the last call in the order in which it was called. Therefore, if we consider the above usage scenario, we need to synchronize all dispatchActions from clickHandler, store them in logical order, and then trigger Fiber’s Re-render merge. How do we store this logic for multiple calls to the same dispatchAction?

A simple way to do this is to use a Queue to store basic information about each logical Update:

typeQueue{last: Update, // Last Update logic dispatch: any, lastRenderedState: any // last render component state}typeUpdate{action: any, // status value next: Update // next Update}Copy the code

Here we use a one-way linked list structure to store the update queue. Once we have this data structure, let’s change the code:

function mountState(initialState){
    lethook = createNewHook(); hook.memoizedState = initalState; // Create a new queue const queue = (hook. Queue = {last: null, dispatch: null, lastRenderedState:null}); // Use closures to share queues among different functions. Bind (null, queue) const dispatch = dispatchAction.bind(null, queue);return [hook.memoizedState, dispatch]
}


functionDispatchAction (queue, action){const update = {action, next: null} dispatchAction(queue, action){// Use data structures to store all update actions so that the latest status values are evaluated in rerenderlet last = queue.last;
    if(last === null){
        update.next = update;
    }else{/ /... // Execute fiber's scheduleWork(); }functionUpdateState (initialState){const hook = updateWorkInProgressHook(); // Calculates the new status value from the update action stored in dispatchAction and returns it to the component (function doReducerWork() {let newState = null;
        do{// Loop the list to perform each update}while(...). hook.memoizedState = newState; }) ();return [hook.memoizedState, hook.queue.dispatch];
} 

Copy the code

At this point, update logic sharing, and we’re done.

Sharing of Hook objects

Hook objects exist relative to components, so in order to realize the sharing of objects in multiple renderings within components, we only need to find a global store that uniquely corresponds to the component global and is used to store all Hook objects. For a React component, the only global storage is naturally ReactNode, which after React 16X should be FiberNode. For simplicity’s sake, we’ll leave Fiber out for now. We just need to know that a component has a unique representation in memory, which we’ll call fiberNode:

typeFiberNode {memoizedState:any // To store all Hook states in a component}Copy the code

Now, the question is, what do we expect from a Function Component? Do we want Function Component’s useState to fully emulate Class Component’s this.setState? If so, our design principles would be:

A function component can only call useState globally once and store all state in a large Object

If that’s all, then the function component has solved the de-class pain point, but we haven’t considered the appeal of gracefully reusing state logic.

Consider a state reuse scenario: We have multiple components that need to listen for resize events in the browser window so that we can retrieve clientWidth in real time. In the Class Component, we either manage the side effect globally and use ContextAPI to deliver updates to child components; Or you have to repeat the logic in the components that use the functionality.

resizeHandler(){
    this.setState({
        width: window.clientWidth,
        height: window.clientHeight
    });
}

componentDidMount(){
    window.addEventListener('resize', this.resizeHandler)
}

componentWillUnmount(){
    window.removeEventListener('resize', this.resizeHandler);
}


Copy the code

The ContextAPI method is definitely not recommended and can cause major maintenance headaches; CTRL + C CTRL + V is even more of a helplessness.

If Function Component can bring us a new ability to reuse state logic, it will undoubtedly bring more imagination to front-end development in terms of reusability and maintainability.

So the ideal usage is:

const [firstName, setFirstName] = useState('James');
const [secondName, setSecondName] = useState('Bond'); // Other non-state hooks, such as providing a more flexible and elegant way to write side effects useEffect()Copy the code

To sum up, the use of multiple hooks for a component should be considered in the design. The challenges are:

We need to store the state of all hooks on fiberNode and make sure they get the correct state up to date every time we re-render

To achieve the above storage goals, the immediate solution is to use a hashMap:

{
    '1': hook1,
    '2': hook2,
    //...
}
Copy the code

Storing in this way requires a unique key identifier to be generated for each call to the hook. This key identifier needs to be passed in from the parameters at mount and update time to ensure that it is routed to the exact hook object.

Add a next attribute to the hook structure:

typeHook = {memoizedState: any, / / the last complete update after the final state of the value of the queue: UpdateQueue < any, any > | null, / / queue next update: Any // next hook} const fiber = {//... memoizedState: { memoizedState:'James', 
        queue: {
            last: {
                action: 'Smith'
            },  
            dispatch: dispatch,
            lastRenderedState: 'Smith'
        },
        next: {
            memoizedState: 'Bond',
            queue: {
                // ...
            },
            next: null
        }
    },
    //...
}
Copy the code

There is a problem with this approach:

The entire list is constructed at mount time, so the order of execution must be maintained during update to route to the correct hook.

Let’s take a rough look at the pros and cons of the two options:

plan advantages disadvantages
hashMap Locating hook is more convenient. There are not too many specifications and conditions for the use of hook The user experience is affected. You need to manually specify the key
The list The API is friendly and concise, and you don’t need to focus on keys Specifications are needed to constrain usage to ensure proper routing

Obviously, the drawbacks of hashMap are intolerable, too expensive and too expensive to use. The specification of the disadvantages of linked list schemes can be guaranteed by tools such as ESLint. In this regard, the linked list won out, and in fact it was the React team’s choice.

Here, we can learn why React Hook specification requires:

Can only be used at the top of function components, not in conditional statements and loops

function Counter(){
    const [count, setCount] = useState(0);
    if(count >= 1){
        const [countTime, setCountTime] = useState(Date.now()); }} // memoizedState: {memoizedState:'0'Queue: {}, next: null} // callsetIn the update phase after Count(1), the corresponding hook object will not be found and an exception will occurCopy the code

Now that we have implemented the React Hooks Class goal, we can use the useState hook to manage state and allow multiple hook calls to function components.

No life cycle problems

In the last section, we implemented the core logic of the React Hook architecture: how to use state in function components with closures, two one-way lists (update lists for single hooks, list of hook calls for components), and the passthrough Dispatch function. So far, we haven’t talked about anything about the life cycle, and this is one of the key issues that our design addresses. We often need to do something before or after a component is rendered, such as:

  • Send an Ajax request in the Class Component’s componentDidMount to pull data from the server.

  • Register and destroy browser event listeners in Class Component componentDidMount and componentDidUnmount.

These scenarios also need to be addressed in React Hook. React has a bunch of lifecycle functions for the Class Component:

  • In the actual project development with more frequent, such as rendering later: componentDidMount, componentDidUpdate, componentWillUnmount;

  • Rarely used pre-render hooks componentWillMount, componentWillUpdate;

  • Has been abused and controversial componentWillReceiveProps and the latest getDerivedStateFromProps;

  • ShouldComponentUpdate for performance optimization;

The React version 16.3 has been made clear in the 17 version abandoned componentWillMount, componentWillUpdate and componentWillReceiveProps these three life-cycle function. Designed to replace the componentWillReceiveProps getDerivedStateFromProps also is not recommended.

Before React Hook, we used to classify component life cycle stages with the technical term render. The name componentDidMount tells us that the component’s DOM is now rendered in the browser and ready to perform side effects. This is obviously technical thinking, so in React Hook, can we get rid of this way of thinking, so developers don’t have to worry about rendering, just know what are side effects, what are states, and what needs caching?

React Hook life cycle solution based on this idea, it should be scenarioized:

ComponentDidMount componentDidUpdate componentWillUnmount componentDidUpdate componentWillUnmount UseEffect () // shouldComponent useMemo ()Copy the code

The advantage of this design is that developers no longer need to figure out when every lifecycle function is triggered and what the effects of processing logic are in it. It’s more about thinking about what are states, what are side effects, and what are complex calculations and unnecessary renders that need to be cached.

useEffect

The full name of effect should be Side effect, which is called Side effect in Chinese. Common Side effects in front-end development include:

  • Dom manipulation

  • Browser event binding and unbinding

  • Sending an HTTP request

  • Print log

  • Accessing system Status

  • Perform I/O change operations

Before React Hook, we used to write these side effects in componentDidMount, componentDidUpdate, and componentWillUnmount, for example

componentDidMount(){
    this.fetchData(this.props.userId).then(data=>{
        //... setState
    })
    
    window.addEventListener('resize', this.onWindowResize);
    
    this.counterTimer = setInterval(this.doCount, 1000);
}

componentDidUpdate(prevProps){
   if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}

componentWillUnmount(){
    window.removeEventListener('resize', this.onWindowResize);
    clearInterval(this.counterTimer);
}
Copy the code

There are some problems with the experience of writing this:

  1. The creation and cleanup logic for the same side effect is scattered in multiple places, which is not a great experience for either writing new code or reading maintenance code.

  2. Some side effects may require multiple copies in multiple places.

The first problem can be solved by placing the clean and new operations in a single function. The clean operation is returned as a thunk function, so we can implement the thunk function every time the effect function is executed:

useEffect(()=>{
    // do some effect work
    return ()=>{
        // clean the effect
    }
})
Copy the code

The second problem, for functional components, is much simpler. We can extract some of the common side effects to form a new function, which can be reused by more components.


function useWindowSizeEffect(){
    const [size, setSize] = useState({width: null, height: null});
    
    function updateSize() {setSize({width: window.innerWidth, height: window.innerHeight});
    }
    
    useEffect(()=>{
        window.addEventListener('resize', updateSize);
        
        return ()=>{
            window.removeEventListener('resize', updateSize); }})return size;
}

Copy the code

Since useEffect is designed to address side effects, the best time to use it is after the component has been rendered to the actual DOM node. This is the only way to ensure that all of the resources (DOM resources, system resources, and so on) needed for side effects are ready.

The example above describes a scenario that requires the same side effect operation to be performed in both mount and update phases. Such a scenario is common and we cannot assume that a single side effect operation during mount will satisfy all business logic requirements. Therefore, in the update phase, useEffect still needs to be re-executed to ensure that it meets the requirements.

This is the real mechanism of useEffect:

Function Component functions (useState, useEffect,…) Every time it is called, all the hook functions inside it are called again.

One obvious problem with this mechanism is that:

Any update to the parent component causes the Effect logic in the child component to be re-executed, which can have a significant impact on performance and experience if there is performance-expensive logic inside Effect.

React has similar optimizations for both the PureComponent and the underlying implementation. As long as no changes are made to the dependent state or props (superficial comparison), rendering is not performed to achieve performance optimization. UseEffect can also be borrowed from this idea:

useEffect(effectCreator: Function, deps: Array)

// demo
const [firstName, setFirstName] = useState('James');
const [count, setCount] = useState(0);

useEffect(()=>{
    document.title = `${firstName}'s Blog`;
}, [firstName])

Copy the code

In the example above, the effectCreator function will not execute as long as the firstName passed in has not changed in the two previous updates. That is, even if setCount(*) is called multiple times, the component will repeat the rendering multiple times, but as long as firstName does not change, the effectCreator function will not repeat the execution.

The realization of the useEffect

UseEffect is implemented basically like useState. Create a hook object at mount time, create a new effectQueue, store each effect as a one-way linked list, bind effectQueue to fiberNode, The effect function stored in the queue is executed after rendering. The core data structure is designed as follows:

typeEffect{tag: any, create: any, deps: Array, destroy: any, deps: Array, next: Effect, // circular list pointer}type EffectQueue{
    lastEffect: Effect
}

typeFiberNode{memoizedState:any}Copy the code

The optimization logic for dePS parameters is simple:

let componentUpdateQueue = null;
functionPushEffect (tag, create, deps){// Build update queue //... }function useEffect(create, deps){
    if(isMount)(
        mountEffect(create, deps)
    )else{
        updateEffect(create, deps)
    }
}

function mountEffect(create, deps){
    const hook = createHook();
    hook.memoizedState = pushEffect(xxxTag, create, deps);
    
}

function updateEffect(create, deps){
    const hook = getHook();
    if(currentHook! ==null){ const prevEffect = currentHook.memoizedState;if(deps! ==null){if(areHookInputsEqual(deps, prevEffect.deps)){
                pushEffect(xxxTag, create, deps);
                return;
            }
        }
    }
    
    hook.memoizedState = pushEffect(xxxTag, create, deps);
}
Copy the code

UseEffect summary

  • ComponentDidMount = componentDidUpdate = componentWillUnmount

  • Mainly used to solve side effects in code, providing a more elegant way to write.

  • Multiple effects are stored in a one-way circular linked list, executed in written order.

  • The DEPS parameter is judged to be exactly the same with the last dependent value by means of shallow comparison. If there is a difference, Effect will be executed again. If there is a difference, the execution of Effect will be skipped.

  • Every time a component is rendered, a clean and effect is created. If there is a clear function return.

  • The cleanup function is executed before the function is created.

useMemo

In useEffect, we use a DEps parameter to declare the effect function’s dependence on the variable, and then use areHookInputsEqual to compare the dePS difference between the two component renderings. If the shallow comparison results in the same, then the effect function is skipped.

If you think about it, isn’t that what the life cycle function shouldComponentUpdate is supposed to do? Why not extract this logic and use it as a generic hook? This is the principle of useMemo hook.

function mountMemo(nextCreate,deps) {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

functionupdateMemo(nextCreate,deps){ const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; // Last cached result const prevState = hook.memoizedState;if(prevState ! == null) {if(nextDeps ! == null) { const prevDeps = prevState[1];if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
Copy the code

The difference between shouldComponentUpdate and useMemo is that useMemo is just a generic caching Hook with no side effects and does not affect whether a component renders or not. So in this sense, useMemo is not a replacement for shouldComponentUpdate, but that doesn’t make useMemo any less valuable. UseMemo provides us with a general performance optimization method. For some performance consuming calculations, we can use useMemo to cache the calculation results. As long as the dependent parameters do not change, the purpose of performance optimization is achieved.

const result = useMemo(()=>{
    return doSomeExpensiveWork(a,b);
}, [a,b])
Copy the code

So what should I do to fully implement shouldComponentUpdate? The answer is react. memo:

Const Button = React. Memo ((props) => {// Your component});Copy the code

This is equivalent to using PureComponent.

So far, getDerivedStateFromProps and other common lifecycle methods have been implemented in React Hook, and componentDidCatch has been officially announced as an implementation. At the end of this section, we’ll look at an alternative to getDerivedStateFromProps.

The purpose of this lifecycle is to update the props passed in by the parent to the component’s state as needed. Although rarely used, it is still possible to do this with React Hook components by calling “setState” once during rendering:

function ScrollView({row}) {
  let [isScrollingDown, setIsScrollingDown] = useState(false);
  let [prevRow, setPrevRow] = useState(null);

  if(row ! PrevRow == prevRow) {// Row has changed since last render. Update the isScrollingDown.setIsScrollingDown(prevRow ! == null && row > prevRow);setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}
Copy the code

If “setState” is called during rendering, the component cancels the render and goes straight to the next one. So it’s important to note that “setState” must be executed in a conditional statement, otherwise it will cause an infinite loop.

Code reuse in React

Those of you who have used earlier versions of React may be aware of the Mixins API, which is officially a more fine-grained logic reuse capability than components. React introduced an ES6-based Class Component that was gradually ‘abandoned’. Mixins, though, can be very convenient and flexible to solve problems of AOP classes, such as reuse of component performance log monitoring logic:

const logMixin = {
    componentWillMount: function(){
        console.log('before mount:', Date.now());
    }
    
    componentDidMount: function(){
        console.log('after mount:', Date.now())
    }
}

var createReactClass = require('create-react-class');
const CompA = createReactClass({
    mixins: [logMixin],
    render: function() {/ /... } }) const CompB = createReactClass({ mixins: [logMixin],
    render: function(){
        //... 
    }
})
Copy the code

But this mode itself will bring a lot of harm, for specific reference to the official blog: “Mixins Considered Harmful”.

React officially recommended embracing HOC in 2016, using higher-order components instead of mixins. The Minxins API can only be used when a component is manually created in create-React-class. This essentially spells the end of mixins as a way to reuse logic.

HOC is so powerful that a number of components and libraries in the React ecosystem use HOC, such as the React-Redux Connect API:

class MyComp extends Component{
    //...
}
export default connect(MyComp, //...)

Copy the code

Implement the above performance log printing using HOC as follows:

function WithOptimizeLog(Comp){
    return class extends Component{
        constructor(props){
            super(props);
           
        }
        
        componentWillMount(){
            console.log('before mount:', Date.now());
        }
        
        componentDidMount(){
            console.log('after mount:', Date.now());
        }
        
        render() {return( <div> <Comp {... props} /> </div> ) } } } // CompAexport default WithOptimizeLog(CompA)

//CompB
export defaultWithOptimizeLog(CompB);

Copy the code

HOC, powerful as it is, is a component in its own right, providing some upper-level capabilities merely by encapsulating the target component, which inevitably leads to the problem of nesting hell. And because HOC is a higher-order mindset that encapsulates reusable logic inside a React component, it’s almost like a magic box that’s bound to be harder to read and understand than a normal React component.

It is certain that HOC mode is a widely accepted logical reuse mode and will be widely used for a long time to come. But with the Introduction of the React Hook architecture, is HOC still appropriate for Function Components? Or are we looking for a new component reuse mode to replace HOC?

The React team says the latter because the React Hook design allows for finer, lighter, more natural and intuitive granularity of logic reuse with functional state management and other Hook capabilities. After all, in the Hook world everything is a function, not a component.

Here’s an example:

export default function Article() {
    const [isLoading, setIsLoading] = useState(false);
    const [content, setContent] = useState('origin content');
    
    function handleClick() {
        setIsLoading(true);
        loadPaper().then(content=>{
            setIsLoading(false);
            setContent(content); })}return (
        <div>
            <button onClick={handleClick} disabled={isLoading} >
                {isLoading ? 'loading... ' : 'refresh'}
            </button>
            <article>{content}</article>
        </div>
    )
}
Copy the code

The code above shows a button with loading state to avoid clicking repeatedly until the load is finished. This component can effectively give feedback to the user and avoid the performance and logic problems caused by the user’s constant attempts to get no good feedback.

Obviously, the loadingButton logic is very generic and business logic-independent, so it is entirely possible to pull it out as a separate loadingButton component:

function LoadingButton(props){
    const [isLoading, setIsLoading] = useState(false);
    
    function handleClick(){
        props.onClick().finally(()=>{
            setIsLoading(false);
        });    
    }
    
    return (
        <button onClick={handleClick} disabled={isLoading} >
            {isLoading ? 'loading... ' : 'refresh'} </button>)function Article(){
    const {content, setContent} = useState(' ');
    
    clickHandler() {return fetchArticle().then(data=>{
           setContent(data); })}return (
        <div>
            <LoadingButton onClick={this.clickHandler} />
            <article>{content}</article>
        </div>
    )
}
Copy the code

Wrapping and extracting a generic UI component into a separate component is a common practice in real business development. This abstraction packages both state logic and UI components into a reusable whole.

Obviously, this is still component reuse thinking, not logical reuse thinking. Imagine another scenario: after clicking loadingButton, I want the body of the article to show a loading state. What should I do?

If loadingButton is not abstracted, it is very easy to reuse isLoading state. The code looks like this:

export default function Article() {
    const [isLoading, setIsLoading] = useState(false);
    const [content, setContent] = useState('origin content');
    
    function handleClick() {
        setIsLoading(true);
        loadArticle().then(content=>{
            setIsLoading(false);
            setContent(content); })}return (
        <div>
            <button onClick={handleClick} disabled={isLoading} >
                {isLoading ? 'loading... ' : 'refresh'}
            </button>
            {
                isLoading
                    ? <img src={spinner}  alt="loading" />
                    : <article>{content}</article>
            }
        </div>
    )
}
Copy the code

But what about abstracted versions of LoadingButton?

function LoadingButton(props){
    const [isLoading, setIsLoading] = useState(false);
    
    function handleClick(){
        props.onClick().finally(()=>{
            setIsLoading(false);
        });    
    }
    
    return (
        <button onClick={handleClick} disabled={isLoading} >
            {isLoading ? 'loading... ' : 'refresh'} </button>)function Article(){
    const {content, setContent} = useState('origin content');
    const {isLoading, setIsLoading} = useState(false);
    
    clickHandler() {setIsLoading(true);
       return fetchArticle().then(data=>{
           setContent(data);
           setIsLoading(false); })}return (
        <div>
            <LoadingButton onClick={this.clickHandler} />
            {
                isLoading
                    ? <img src={spinner}  alt="loading" />
                    : <article>{content}</article>
            }
        </div>
    )
}
Copy the code

The problem is not made any easier by abstraction. The parent component, Article, still needs to have its own isLoading state, which is not elegant enough. So what’s the point?

The answer is coupling. The above abstraction scheme couples isLoading state and button tag into a component. The granularity of reuse can only be used for the whole component, not for a single state. The solution is:

// Provide an abstraction of the loading stateexport function useIsLoading(initialValue, callback) {
    const [isLoading, setIsLoading] = useState(initialValue);

    function onLoadingChange() {
        setIsLoading(true);

        callback && callback().finally(() => {
            setIsLoading(false); })}return{value: isLoading, disabled: isLoading, onChange: onLoadingChange, // adapt other components onClick: onLoadingChange, // adapt button}}export default function Article() {
    const loading = useIsLoading(false, fetch);
    const [content, setContent] = useState('origin content');

    function fetch() {
       return loadArticle().then(setContent);
    }

    return( <div> <button {... loading}> {loading.value ?'loading... ' : 'refresh'}
            </button>
           
            {
                loading.value ? 
                    <img src={spinner} alt="loading" />
                    : <article>{content}</article>
            }
        </div>
    )
}
Copy the code

This allows for a more fine-grained reuse of state logic, from which you can decide whether to further encapsulate UI components, depending on the situation. For example, it is still possible to wrap a LoadingButton:

// Encapsulate the buttonfunction LoadingButton(props){
    const {value, defaultText = 'sure', loadingText='Loading... '} = props;
    return( <button {... props}> {value ? LoadingText: defaultText} </button>)} // Encapsulate loading animationfunction LoadingSpinner(props) {
    return (
        < >
            { props.value && <img src={spinner} className="spinner" alt="loading"/>} </>)} // usereturn( <div> <LoadingButton {... loading} /> <LoadingSpinner {... loading}/> { loading.value || <article>{content}</article> } </div> )Copy the code

Reuse at the state logic level brings a new capability to component reuse, which depends entirely on React Hook’s function-based component design, where everything is a Function call. And Function Component is not exclusive of HOC, you can still use familiar methods to provide higher level capabilities, but now you have another weapon in your hand.

Align the capabilities that the React Class component already has

As of this writing, there are still some Class Component features that React Hook doesn’t have, such as the lifecycle functions componentDidCatch and getSnapshotBeforeUpdate. There are also some third-party libraries that may not be compatible with hooks.

We’ll make it up as soon as possible

The future is there. We just have to wait.

Reference documentation

  • React Hook Official documentation

  • Under the hood of React’s hooks system

  • Into the React Hook system

  • React: Fiber

  • Analyze the implementation process of useState from the source code

  • The React Fiber architecture

  • The difference between arrays and linked lists

  • React Goes from Mixin to HOC to Hook