Componentization brings unprecedented fluency to the front end, and we can use these components to assemble our entire UI. Faster development also increases maintainability. However, as the complexity of business functions increases, more and more repetitive logic code appears, and we have to copy and paste and maintain multiple pieces of code. To address this issue, the React team has explored logic reuse solutions, from mixins to higher-order components to Render Props and now hooks.

React Hooks Are a new feature in React 16.7.0-Alpha. In this article, I will compare the Class syntax with the Hook syntax. I will focus on the use of Hook syntax and express the advantages and advantages of Hook. Do not use functionalhooks. Do not use functionalhooks.

Status of class components

The Class component (Class syntax) is by far the most widely used way to write React. Common problems are

  • Inheriting pure components causes rendering invalidation, which is common when the same object keeps the same reference.
  • Class’s this and super syntax is not very friendly, and Babel’s final compiler is still a function. Also, Class’s mere use as syntactic sugar makes it difficult to optimize React internally.
  • React syntheses events and executes functions in strict mode. Bind or the arrow function can be used to solve the problem

  • Life cycle redundancy, in some cases there will be componentDidMount and componentDidUpdate to execute similar code fragments, more life cycle, 16 and launch a new life cycle, learning cost is large. Meanwhile, to learn the new life cycle, we need to understand the design philosophy inside React, and understand the separation of reconciliation and commit phases and their meanings.
  • High-order components are weak, which is essentially to enhance component functions and realize logic reuse. In most cases, these are props for controlling child components, with little need for hijack, overwrite, or super.render() of the child lifecycle. At the same time, because higher-order components must use the way of component nesting, the DOM structure of several more layers is easy to become a nesting structure.
  • In my opinion, the biggest problem is the component reuse dilemma. In the early stage, we used CreateClass + Mixins, and after replacing CreateClass with Class syntax, we designed Render Props and HOC. The React team continued to work on reusing components until the Function Component + Hooks design. Hooks are not the end.

We also tend to write our components more and more view-based. Components are slowly divided into parts of the view (there’s nothing wrong with that, of course), with props or Redux as connections. When different components have similar logic, it is implemented either through higher-order components or functions. Since functions (without hooks) do not have setState capabilities (of course you pass in a forceUpdate function), stateless components have high performance but are not powerful enough to be used as simple display components.

So for elegant reuse of logic and a more simplified API, as well as better performance optimization control within React, Hook emerged.

Hook introduction

The API is relatively simple:

  • UseState control status;
  • UseEffect controls the life cycle;
  • UseCallback does function caching;
  • UseMemo does data structure caching to prevent child components from failing in Pure;
  • UseRef maintains data.

You can use Redux for centralized state management or useContext + useReducer.

Another thing you might use is useImperativeHandle + forwardRef to make it easier for the parent component to get values and functions directly from the child component, since there are no instances of the function component.

At first glance the React code using Hooks might wonder if creating so many inline functions would affect performance. Didn’t React always advise against creating new functions in callback? First, take a look at the official explanation, which states that the performance of closure functions in JavaScript is very fast and not much worse due to the lightweight function components compared to Class and the avoidance of additional layers of HOC, renderProps, etc.

advantages

Hooks are now highly regarded and indeed have obvious advantages over class components.

  • Life cycle simplified, a useEffect + dependent array goes around the world. Weakened concept of life cycle. Not only is it easier to get started, it also makes a qualitative difference in the way code is written. From the obvious side effects of class pattern development to pure declarative function development.
  • More functional programming, eliminating some of the side effects of Class (optimized for internal scheduling of fiber structures). A similar effect to the last one.
  • Hooks provide an API that makes it easier to build your Redux application to fit your needs.
  • More elegant logic reuse than HOC and props patterns. In the case of class components, the Render function must return a JSX structure or NULL, which is obviously unfriendly to logical abstraction.

Elegant reuse of logic would lead to a much more robust ecosystem, with more people willing to keep their logic out of hooks (because it’s so elegant), publish it in libraries, and add to the React ecosystem (look at all the hook-based state management tools these days). Such as the hooks.

  • The model is more declarative.

Old thought: “I’m going to check props.A and state.B (props and state) during this lifecycle, and trigger XXX side effects if they change.” This kind of thinking is easy to miss the check items and cause bugs when modifying logic in the future.

New thought: “My component has A side effect of XXX, which depends on data like props.A and state.b”. From the old imperative to declarative programming.

In fact, if you think about it, didn’t people use life cycles in the past to decide whether or not to perform side effects? Hooks now give you an API to declare side effects directly, making the lifecycle a “low-level concept” that doesn’t need to be considered by developers. Developers work at a higher level of abstraction.

In a similar way, React also provides an API for declaring “complex calculations” (useMemo), instead of doing dirty checks during the life cycle and caching results in state.

  • Mental models of functional components.

A functional component is not just “another way to write a class component”; it has a very different mental model from a class component. ** Functional components are still mappings of external data => View, and are still the mental model of pure Function. ** Only now the external data includes not only props and context, but also state.

Above said so many formulaic words, the actual experience, writing is the biggest change, the way to implement functions compared to the class way has a great change. Life cycle this side effect set is weakened, pure functional, declarative writing personally feel more regression JS language itself, the characteristics of the language itself can be mined, function function has been greatly expanded.

The benefit of class component writing is that it has evolved over time. How a page should be written, whether redux is used or not, what repetitive logic can be HOC pulled out of it, and which components are common can be determined by understanding the view and functionality of the page. Hooks are not. Logic can be pulled out of class components. Without HOC’s limitations, you can pull out automatically or trigger.

The advantages of being simpler or more straightforward are:

  • Multiple states do not create nesting, it is still tiled (solve the nesting problem of HOC)
  • Allows function components to use state and partial lifecycle (gives function react capability)
  • Easier separation of component UI from state (biggest benefit)

Use explore

Hook is a form of logic reuse provided by React framework. In many cases, developers tend to be superficial. They only experience the pleasure of functional programming but fail to achieve logic reuse, or go too far, and lack design sense and reuse in the process of reuse.

The following ideas only provide a thought, which does not necessarily mean that the practice is right. We hope that everyone can have their own thinking and discuss together, and explore more best practices of Hook.

Particle composite

In the normal development process, state is defined in the class component’s state (some non-state data sometimes exists in state), request data is defined in the lifecycle and then the logic of updating the state view is updated. Finally, the various custom functions mounted above State and this in Render are applied to the JSX structure.

Now switch to Hook development, you can develop your functionality using the same structure as above, or think more.

const ComponentA = props => {
    const {
        state: {},
        data: {},
        action: {}
    } = useLogic(props);

    return <div></div>;
};
Copy the code

In the above structure, useLogic encapsulates the full capabilities of the component and returns state, static data, and operational functions. UseLogic is a logical extraction of ComponentA. UseLogic is a logical extraction of ComponentA. Of course, logical decoupling should be reasonable, not simply to put all the logical functions in another file, where the only benefit might be to reduce the number of lines of code in the file.

In this case, useLogic is too much coupled with the function of ComponentA, so it does not have generality. The withdrawal without generality may have little significance. (Just reduced the number of lines of code for ComponentA)

const ComponentA = props => {
    const {rdData} = useRequest(props)
    
    const {
        state: {},
        data: {},
        action: {}
    } = useLogic(rdData);

    return <div></div>;
};
Copy the code

Now stripping out the request part of useLogic, which tends to be extremely coupled to page functionality, useRequest takes the basic arguments and makes the request, setState when it gets the return value, and ComponentA rerenders. At this point useLogic takes the back-end data and reprocesses the logic.

So far, pages can simply separate the interface request part from the data processing part. Thinking outside the box, you can package your page’s list data lazy loading, drag and drop, countdown, answer logic, progress bar logic into separate hooks, or even into some kind of general function, such as chart rendering, boss common table manipulation logic, paginating logic.

So, going back to the code itself, there’s actually a question of where to store state, you can store it in ComponentA, so useLogic does a bunch of things with state that’s passed in, Need to use useCallback, useMemo to cache many variables (avoid repeated render). In this case, useLogic does not have setState capabilities. It is weak but flexible and suitable for simple interaction functions.

const ComponentA = props => {
    const [pageData, setPagedate] = useState({})
    const {
        state: {},
        data: {},
        action: {}
    } = useLogic(pageData);

    const {rdData} = useRequest(props);
    setPagedate(rdData)

    return <div></div>;
};
Copy the code

At this point useLogic is a simple run-time data + operation factory, but this approach does not provide sufficient separation of logic when state is complex and run-time data is abundant.

At this point, you can choose to save state in useLogic, which seems more like a custom hook. UseLogic manages its own state and exposes data layer + operations.

const useLogic = data= > {
    const [state, setState] = useState({});

    useEffect(() = > {
        if (data) {
            setState();
        }
    }, [data]);

    return {
        state: {state},
        data: {},
        action: {setState}
    };
};
Copy the code

At this point useLogic is strongly coupled to ComponentA’s functionality, and any new functionality is basically modified in useLogic, so for universality (if you don’t want the useLogic to be reused elsewhere), The functional dimensions of useLogic must be small, and the granularity of logic extraction must be sufficient (but not too small).

const ComponentA = props => {
    const [pageData, setPagedate] = useState({})
    
    const {
        state: {},
        data: {},
        action: {}
    } = useLogic1(pageData);
  
    const {
        state: {},
        data: {},
        action: {}
    } = useLogic2(pageData);
  
    const {
        state: {},
        data: {},
        action: {}
    } = useLogic3(pageData);

    const {rdData} = useRequest(props);
    setPagedate(rdData)
  
    return <div></div>;
};
Copy the code

Another problem arises at this point, functional relevance. UseLogic1 needs to linkage trigger a function of useLogic2 or obtain data. In this case, most rely on the function of ComponentA as binder to integrate the coupling module of independent function.

The page component design grain mentioned above is based on the business level, with the right combination of pull +, we can make the page code style more concise, logical abstraction more elegant. Perhaps the only question is where the right point is.

layered

Here’s a look at layering in overall architecture design, which is a controversial proposition in the React Hooks architecture design article:

Although there are various forms of layering, I choose a more traditional method that can better express the essence of the program, so that the hook is divided into 6 layers vertically, from bottom to top:

  1. The lowest level of built-in hook, do not need to implement, the official directly provided.
  2. Hook to simplify the way of status update, the more classic is to introduce immer to achieve more convenient for the purpose of unchangeable new.
  3. The concept of “state + behavior” is introduced to quickly create a complete context by declaring state structures and corresponding behaviors.
  4. Encapsulates operations on common data structures, such as groups of operations.
  5. Encapsulate for common business scenarios, such as paged lists, scrollloaded lists, multiple selections, and so on.
  6. Actual business-oriented implementation.

The layered design at the architectural level is Hook free, which is what it should be because hooks are the icing on the cake. The basic generality of a page should have been provided at the bottom, to every consumer through routing or other tools. Hook itself only provides the most basic functions, on which we can fully implement a layer of our own business use, such as the useRefState mentioned below. We can simulate the way we want with some ideas like VCA (for example, I copied the Vue Composition API to learn React Hooks), such as reactive with proxies, synchronous Hook execution with generator (difficult).

Vue3 Composition API

In general, the encapsulation of custom Hook should be based on the functional dimension, and the independent functions of the page should be removed, such as countdown function, registration logic, and logical combination in the page. Yes, this may be another reason for you to write Hook, because combination is better than inheritance. The encapsulation of custom hooks should not be blind, but should take full account of decoupling of functions and future changes in requirements.

But at the same time, the problems of Hook are also obvious.

defects

Hook is very powerful, but there are some not so beautiful places in use. The following is my ridicule link.

The repetitive execution area of a function is too large, which may cause performance problems.

Each time a class component renders, it repeats the partial lifecycle and the render function, whereas for a function component, render is a re-execution. So local variables that you define are reassigned if they are not linked to the Hook API itself. You can either upgrade to global or save once using caching functions such as useRef.

Repeated execution, repeated generation of new objects or functions will naturally invalidate the child component’s Pure optimization.

For example:

let Son = ({data: {aaa}}) => { console.log('son render'); return ... }; // Son = memo(Son); Const father = () => {// every father renders a new data object const data = {aaa: 111}; return ( <div > <Son data={data} /> </div> ); };Copy the code

Although son has been shallow by memo and aaa in the data passed remains unchanged each time, the child component must be rerendered whenever the father component is rerendered. This is because each time data is a newly generated object, and both Pure and Memo are only shallowEqual comparisons, the different reference objects result in a direct return of false, and the subcomponent is re-rendered. Although an unnecessary render may not cause a big problem, it is a quality that every coder should have to optimize the code level as much as possible.

The right thing to do, of course, is to use the useMemo and useCallback mentioned above. Remember that using the memo with the memo works best. Of course, if you don’t need to use this optimization, relying on a few comparisons and closures in an array is much more expensive than defining an inline function.

Another point worth noting is that hooks use object. is internally to compare new and old states for equality.

No suitable global variable exists

Similar to the above problem, since each render execution area is the entire function, you don’t have a global variable inside the function without using the Hook API. Of course, you can define this outside of the function to solve this problem, or you can create a global variable based on the useRef function, like this in the class component, which can pass arguments to internal functions, like this in the class component, which is not used as state.

Closures everywhere

In a nutshell, look at the figure below

Variables that are accessed internally and externally during the execution of a function defined within a function are stored based on closures. The value of a accessed by test2 above is the value in the closure.

So every inline function you write in a Hook that accesses external variables inside of it is going to be the value that you get in closure mode. There’s nothing wrong with that in itself, but if your function is a cached or only bound event once, then you must be accessing the value copied into the heap by the pre-parser, which is the old value.

This is necessary in timers, usecallBacks, and events that bind only once. The solution can be to add dependency arrays, but adding dependency arrays is not a cure, and it also interferes with semantics, and can be ugly when there are too many dependencies. In some cases you can circumvent these problems by using the useState function syntax or using useReducer. For now, the most common way to save is through useRef, which constructs a global object outside the hook rendering cycle. You can use useRef to save a single value (one will suffice in most scenarios), or you can construct a global large object and manually save all state references.

My current approach is to construct a new layer of API based on native setState:

/** * useRefState references the latest value of state * @param {any} initialState ** Demo: * const [count, setCount, CountRef] = useRefState(0) * countref. current Get the latest value */ export const useRefState = initialState => {const ins = useRef(); Const [state, setState] = useState(() => {// Initialize const value = isFunction(initialState)? initialState() : initialState; ins.current = value; return value; }); const setValue = useCallback(value => { if (isFunction(value)) { setState(prevState => { const finalValue = value(prevState); ins.current = finalValue; return finalValue; }); } else { ins.current = value; setState(value); }} []); return [state, setValue, ins]; };Copy the code

API usage conditions are strict

The most important rule is to call a Hook only on the outermost layer of a function. Do not call in loops, conditional judgments, or subfunctions because of the implementation of hooks. This condition means that your custom hooks must be written directly outside of the component without any logical judgment. This requirement is a bit more difficult and inconvenient for beginners.

A custom Hook that generates new data based on the backend data, regardless of whether the backend data is retrieved, must be written directly in the outer layer of the component, all the judgment must be inside the custom Hook.

const ComponentA = props => {
    const {rdData} = useRequest(props);

    const {
        state: {},
        data: {},
        action: {}
    } = useLogic(rdData);

    return <div></div>;
};
Copy the code

UseLogic internally determines whether or not rdData has been retrieved, and if this is a general function, whether or not the rdData has a value may be slightly more cumbersome than a field.

Reporting errors is not friendly

If you break the rules mentioned above, you may learn:

The same problem may cause a different error, this problem can be said to be a disaster level. It is extremely important to install the appropriate code checking plugins, such as using ESLint and enabling the relevant rules.

Not suitable for heavy pages

Combined with the above reasons, in fact, a heavy page of the main page, in fact, is not suitable for Hook writing, of course, split out of the widget can be written. There will be no problem if the main logic of the page is in the main page, which is responsible for state creation and modification, distribution of modified state function and sub-component data transfer. It is inevitable to use a lot of useMemo and useCallback. And closed mines buried in the ground.

There are too many data sources to guarantee that a runtime data will fetch the correct value in subsequent iterations. You may be ok, but the next person to take over will need to modify the dependency data or reference the value via a global variable. Occasionally use rise still it doesn’t matter a problem, but much rise unavoidably some drink poison to quench thirst feeling.

Understand the difficulties

I am not aware of this, but many people online ridicule logic encapsulation after reading becomes difficult to understand. This should be related to the rationality of logical packaging, or as mentioned above, packaging should be an independent functional dimension, not too thick particles should not be too fine,

The cheat useRef

I don’t know if this is a defect or not, but using useRef to achieve the cheating effect is the way of track in my opinion (but it smells good). Expect this issue to be addressed by the React team in the future, with a new API or something like that. With full functional programming, it is a little weak in complex scenarios (or maybe I am too weak), data passing + closure issues can make parameters very complex, in this case Ref as the cheat global variable to solve all problems. The value generated by useRef can simply be interpreted as a global variable outside the function. React provides the value to ensure that it does not change during the rendering cycle.

In the future

There is no need to tangle with OOP or FP, THE characteristics of JS itself, for the complete implementation of OOP itself is not easy, the powerful flexibility of functions in JS, as the status quo of first-class citizens also let the use of functional become respected. However, in Hook, you can still use some OOP thinking to develop, in short, the implementation of the method or technique is your own decision, do not ignore the use of the situation and excessive preference.

If you choose Function Component, you choose both good and bad functions. The advantage is that it is powerful and can mimic almost anything you want, the disadvantage is that it can be combined flexibly, and if custom Hooks naming and implementation are not standard enough, the cost of communicating between functions is much higher.

According to Dan, Hook is the future of React. There are also some issues with Hooks that should not be ignored. Click on the ugly side of React Hooks for a brief look. Hook writing is not difficult, the difficulty is to write well.

In the future, the public libraries of Hooks will inevitably have the same level as ANTD, but the best practices of layering and granular composition are still a direction of exploration, but there is no doubt that this will unleash endless creativity.

Finally, this paper does not cover another important function of Hook: useReducer and useContext. If there is an opportunity to introduce next time, including but not limited to the implementation of REdux management tool based on Hook API, logical component CONNECT.

Please join us in the comments section.

Further reading

  • Imperative vs. Declarative programming
  • Baked through the React Hook

reference

  • Why does React implement functional components now? Is it bad to use class?
  • The React website
  • Implement algebraic effects with class hooks programming