Introduction to React Hooks: Foundation

Keywords: Javascript React Attribute github

Preface

First of all, you are welcome to pay attention to me. Github blog It's also a little encouragement for me. After all, writing can't be realized. It depends on your enthusiasm and encouragement to stick to it. I hope you pay more attention to it. React 16.8 has added Hooks features, and React official documents have added Hooks module to introduce new features. It shows how much attention React attaches to Hooks. If you don't know what Hooks are, it's strongly recommended that you know. After all, this may be the future direction of React.
  

origin

React has always had two ways of creating components: Function Components (Function Components) and Class Components (Class Components). The function component is just a normal JavaScript function that accepts the props object and returns React Element. In my opinion, function components are more in line with React's idea, data-driven view, without any side effects and states. In applications, functional components are usually used only by very basic components, and you will find that as business grows and changes, components may have to contain state and other side effects, so you have to rewrite previous functional components to class components. But things are often not so simple, class components are not as good as we think, in addition to the incremental workload, there are other problems.

First, class component sharing state logic is very cumbersome. For example, we borrow a scenario from an official document where the FriendStatus component is used to show whether the user is online in the list of friends.

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(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 FriendStatus component above actively subscribes to user status at creation and unsubscribes when uninstalled to prevent memory leaks. Assuming that another component also needs to subscribe to the user's online state, if we want to reuse the logic, we usually use render props and higher-order components to reuse the state logic.

// Reuse state logic in render props
class OnlineStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(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() {
    const {isOnline } = this.state;
    return this.props.children({isOnline})
  }
}

class FriendStatus extends React.Component{
  render(){
    return (
      <OnlineStatus friend={this.props.friend}>
        {
          ({isOnline}) => {
            if (isOnline === null) {
              return 'Loading...';
            }
            return isOnline ? 'Online' : 'Offline';
          }
        }
      </OnlineStatus>
    );
  }
}
// Reuse of state logic by means of higher-order components
function withSubscription(WrappedComponent) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = { isOnline: null };
      this.handleStatusChange = this.handleStatusChange.bind(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() {
      return <WrappedComponent isOnline={this.state.isOnline}/>
    }
  }
}

const FriendStatus = withSubscription(({isOnline}) => {
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
})

The above two ways of reusing state logic not only require time-consuming and laborious component reconstruction, but also Devtools will find that the component hierarchy deepens when they look at the component hierarchy. When the reused state logic is too much, they will fall into the situation of component nesting hell. It can be seen that the two methods mentioned above can not solve the problem of state logic reuse perfectly.

Moreover, as business logic in class components becomes more complex, maintenance becomes more difficult, because state logic is divided into different life cycle functions, such as subscription state logic in component DidMount, cancellation subscription logic in component WillUnmount, and the code of related logic is fragmented, while the code of unrelated logic is likely to be centralized. Together, the whole is not conducive to maintenance. And class component learning is more complex than functional components, so you need to be on guard against this trap in components and never forget to bind this to event handlers. For all these reasons, it seems that function components still have their own advantages.

Hooks

Functional components have always lacked the characteristics of class components, such as state, life cycle and so on. The emergence of Hooks is to let functional components have the characteristics of class components. Official definition:

Hooks are functions that let you "hook into" React state and lifecycle features from function components.

To make a function component have the characteristics of a class component, the logic of state must be implemented first.

State: useState useReducer

  useStateNamely React Provide the most basic and commonly used Hook,Mainly used to define the local state, let's take a simple counter as an example.:

import React, { useState } from 'react'

function Example() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <span>{count}</span>
      <button onClick={()=> setCount(count + 1)}>+</button>
      <button onClick={() => setCount((count) => count - 1)}>-</button>
    </div>
  );
}

UseState can be used to define a state, which, unlike state, can be not only an object, but also an underlying type value, such as the Number type variable above. UseState returns an array, the first is the actual value of the current state, and the second is a function to change that state, similar to setState. The update function is the same as setState in that it accepts both values and parameters of the function. Unlike useState, the update function replaces the state rather than merges it.

If there are more than one state in a function component, the state of the object type can be declared either by one useState or by many times by useState.

// Declare the state of the object type
const [count, setCount] = useState({
    count1: 0,
    count2: 0
});

// Multiple statements
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);

Compared with declaring the state of the object type, it is obviously more convenient to declare the state many times, mainly because updating functions is a replacement method, so you have to add unchanged attributes to the parameters, which is very troublesome. It should be noted that React records the internal states by the order of Hook calls, so Hook can not be invoked in conditional statements (such as if) or circular statements, and it should be noted that we can only invoke Hook in function components, but not in components and ordinary functions (except custom Hook).

When we are dealing with complex multi-layer data logic in function components, we are not able to use useState. Fortunately, React provides us with useReducer to deal with complex state logic in function components. If you have used Redux, useReducer is very kind. Let's rewrite the previous counter example with useReducer:

import React, { useReducer } from 'react'

const reducer = function (state, action) {
  switch (action.type) {
    case "increment":
      return { count : state.count + 1};
    case "decrement":
      return { count: state.count - 1};
    default:
      return { count: state.count }
  }
}

function Example() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  const {count} = state;
  return (
    <div>
      <span>{count}</span>
      <button onClick={()=> dispatch({ type: "increment"})}>+</button>
      <button onClick={() => dispatch({ type: "decrement"})}>-</button>
    </div>
  );
}

UseReducer accepts two parameters: the reducer function and the default value, and returns an array of the current state state and dispatch functions, whose logic is basically the same as that of Redux. The difference between useReducer and Redux is that the default value of Redux is given by assigning default parameters to the reducer function, for example:

// Default Value Logic of Redux
const reducer = function (state = { count: 0 }, action) {
  switch (action.type) {
    case "increment":
      return { count : state.count + 1};
    case "decrement":
      return { count: state.count - 1};
    default:
      return { count: state.count }
  }
}

useReducer does not use Redux logic because React believes that the default value of state may come from props of function components, such as:

function Example({initialState = 0}) {
  const [state, dispatch] = useReducer(reducer, { count: initialState });
  // Omission...
}

This allows you to pass props to determine the default value of state, although React does not recommend the default value of Redux, but it also allows you to assign default values in a Redux-like way. This involves touching the third parameter of useReducer: initialization.

As the name implies, the third parameter initialization is used to initialize the state. When the user reducer initializes the state, the second parameter initialState is passed to the initialization function. The value returned by the initialState function is the initial state of the state, which allows a function to be abstracted out of the reducer specifically responsible for calculating the initial state of the state. For example:

const initialization = (initialState) => ({ count: initialState })

function Example({initialState = 0}) {
  const [state, dispatch] = useReducer(reducer, initialState, initialization);
  // Omission...
}

So with the help of initialization function, we can simulate the initial value of Redux:

import React, { useReducer } from 'react'

const reducer = function (state = {count: 0}, action) {
  // Omission...
}

function Example({initialState = 0}) {
  const [state, dispatch] = useReducer(reducer, undefined, reducer());
  // Omission...
}

Side Effects: useEffect useLayoutEffect

The definition of internal state in function component is solved, and the problem of life cycle function in function component is solved urgently. In functional React, life cycle functions are bridges between functional and imperative functions. You can perform Side Effects in your life cycle, such as requesting data, manipulating DOM, etc. React provides useEffect to deal with side effects. For example:

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`
    return () => {
      console.log('clean up!')
    }
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

In the example above, we pass in a function to useEffect and update the page title according to count value in the function. We will find that the callback function in useEffect is called every time the component is updated. So we can think of useEffect as a combination of component DidMount and component DidUpdate. Callback functions are called when components are mounted and updated. Looking at the example above, the callback function returns a function that is specifically designed to eliminate side effects. We know that side effects like listening events should be cleared in time when components are unloaded, otherwise memory leaks will occur. Clearance functions are called before each component is rendered again, so the order of execution is:

render -> effect callback -> re-render -> clean callback -> effect callback

So we can use useEffect to simulate componentDidMount, componentDidUpdate, and componentWillUnmount behavior. As we mentioned earlier, because of the life cycle function, we have to split the relevant code into different life cycle functions, instead of putting the relevant code in the same life cycle function. The main reason for this is that we do not write code based on business logic, but through execution time coding. To solve this problem, we can solve the above problem by creating multiple Hooks and placing the relevant logic code in the same Hook:

import React, { useState, useEffect } from 'react';

function Example() {
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  
  useEffect(() => {
    otherAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return function cleanup() {
      otherAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // Omission...
}

We focus our logical concerns on multiple Hook s to avoid the confusion of unrelated code. But then there is a problem. Assuming that one of our actions is determined to be executed only when the component DidUpdate or component DidMount is distinguished, can useEffect distinguish? Fortunately, useEffect provides us with a second parameter. If the second parameter is passed into an array, the callback function in useEffect will be executed only if the value in the array changes when rendering again. So if we pass an empty array to it, we can simulate the life cycle component DidMount. But if you want to simulate componentDidUpdate only, no good method has been found for the time being.

The difference between useEffect and class component life cycle is that both component DidUpdate and component DidMount are executed synchronously after DOM updates, but useEffect does not execute synchronously after DOM updates, nor does it block the update interface. If you need to simulate lifecycle synchronization, you need to use useLayout Effect, which is the same as useEffect, and the area is only on execution time.

Context: useContext

With Hook:useContext, we can also use context in function components. UseContext is more convenient than render props in class components.

import { createContext } from 'react'

const ThemeContext = createContext({ color: 'color', background: 'black'});

function Example() {
    const theme = useContext(Conext);
    return (
        <p style={{color: theme.color}}>Hello World!</p>
    );
}

class App extends Component {
  state = {
    color: "red",
    background: "black"
  };

  render() {
    return (
        <Context.Provider value={{ color: this.state.color, background: this.state.background}}>
          <Example/>
          <button onClick={() => this.setState({color: 'blue'})}>color</button>
          <button onClick={() => this.setState({background: 'blue'})}>backgroud</button>
        </Context.Provider>
    );
  }
}

useContext accepts the context object returned by the function React.createContext as a parameter and returns the current context median. When the value in Provider changes, the function component will be re-rendered. It is important to note that even if the unused value of context changes, the function component will be re-rendered. As in the example above, Example will re-render even if the background is not used in the Example component, but when the backgroundchanges. So if necessary, if the Example component also contains subcomponents, you may need to add shouldComponentUpdate to prevent unnecessary rendering waste performance.

Ref: useRef useImperativeHandle

useRef is commonly used to access instances of child elements:

function Example() {
    const inputEl = useRef();
    const onButtonClick = () => {
        inputEl.current.focus();
    };
    return (
        <>
            <input ref={inputEl} type="text" />
            <button onClick={onButtonClick}>Focus the input</button>
        </>
    );
}

As we said above, useRef is often used on ref attributes, but in fact useRef does more than that.

const refContainer = useRef(initialValue)

  useRefYou can accept a default value and return a containingcurrentA variable object of an attribute that will last through the life cycle of the component. So it can be used as an attribute of a class component.

  useImperativeHandleUsed to customize exposures to parent componentsrefAttribute. Need to cooperateforwardRefUse them together.

function Example(props, ref) {
    const inputRef = useRef();
    useImperativeHandle(ref, () => ({
        focus: () => {
            inputRef.current.focus();
        }
    }));
    return <input ref={inputRef} />;
}

export default forwardRef(Example);
class App extends Component {
  constructor(props){
      super(props);
      this.inputRef = createRef()
  }
  
  render() {
    return (
        <>
            <Example ref={this.inputRef}/>
            <button onClick={() => {this.inputRef.current.focus()}}>Click</button>
        </>
    );
  }
}

New Feature: useCallback useMemo

Students familiar with React have seen similar scenes:
  

class Example extends React.PureComponent{
    render(){
        // ......
    }
}

class App extends Component{
    render(){
        return <Example onChange={() => this.setState()}/>
    }
}

In this scenario, although Example inherits PureComponent, it does not optimize performance because each onChange attribute passed in by an App component is a new function instance, so each Example is rendered anew. In order to solve this problem, we usually adopt the following methods:

class App extends Component{
    constructor(props){
        super(props);
        this.onChange = this.onChange.bind(this);
    }

    render(){
        return <Example onChange={this.onChange}/>
    }
    
    onChange(){
        // ...
    }
}

The above method solves two problems at the same time. Firstly, it ensures that the onChange attribute passed to Example component is the same function instance every time it is rendered, and solves the binding of callback function this. So how to solve this problem in function components? React provides the useCallback function to cache event handles.

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

useCallback accepts functions and an array input, and returns a cached version of the callback function. Only when the values in the array change when the re-rendering occurs, will it return a new function instance. This also solves the problem of optimizing the performance of the sub-components mentioned above, and there will be no cumbersome steps mentioned above.

Similar to useCallback, useMemo returns a cached value.

const memoizedValue = useMemo(
  () => complexComputed(),
  [a, b],
);

That is, the callback function recalculates the cached data only when the values in the array change during the re-rendering, which allows us to avoid complex data calculations every time we re-render. So we can think that:

useCallback(fn, input) is equivalent to useMemo (() => fn, input)

If no second parameter is passed to useMemo, useMemo will only be recalculated when new function instances are received. It should be noted that React official documentation reminds us that useMemo can only be used as a means of optimizing performance, not as a semantic guarantee. That is to say, React can also be used in some cases even if the data in the array has not changed. It will be re-executed.

Custom Hook

As we mentioned earlier, Hook can only be invoked at the top of a function component, not in a loop, condition, or normal function. As we mentioned earlier, it's very cumbersome for class components to share state logic, and they have to resort to render props and HOC. In contrast, React allows us to create custom Hooks to encapsulate shared state logic. A custom Hook is a function that starts with a function name and calls other Hooks. We use a custom Hook to rewrite an example of the initial subscriber status:

function useFriendStatus(friendID) {
    const [isOnline, setIsOnline] = useState(null);

    function handleStatusChange(isOnline) {
        setIsOnline(isOnline);
    }

    useEffect(() => {
        ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
        return () => {
            ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
        };
    });

    return isOnline;
}

function FriendStatus() {
    const isOnline = useFriendStatus();
    if (isOnline === null) {
        return 'Loading...';
    }
    return isOnline ? 'Online' : 'Offline';
}

We rewrite previous examples of subscribers'online status with custom Hook. Compared with render prop and HOC's complex logic, custom Hook is more concise. Not only that, but also custom Hook does not cause the case of wrapper hell mentioned earlier. Elegant solution to the previous class component reuse state logic difficult situation.

summary

With the help of Hooks, function components can basically realize the functions of most class components. Not only that, Hooks has certain advantages in sharing state logic and improving the maintainability of components. Predictably, Hooks is likely to be React's big predictor for the future. React officially adopts Gradual Adoption Strategy for Hook, and says there is no plan to remove classes from React at present. Hooks will work in parallel with our existing code for a long time. React does not recommend that we rewrite all the previous class components with Hooks, but suggests that we use Hooks in new or non-critical components.
  
If the expression is inadequate, accept criticism and advice modestly. May you all make progress together!

Posted by willeh_ on Wed, 08 May 2019 16:45:39 -0700