What is React Hooks

Recently, when reconstructing the management page of BadJS, I used the technology stack of TypeScript + React Hooks to take advantage of this opportunity to deal with React Hooks.

React Hooks is a new feature of version 16.7.0-alpha, which can be enjoyed after installation.

About React Hooks

React Hooks is an extension of React function components. Through some special functions, stateless components have the capabilities that only stateful components have.

Hooks is a special function in the React function component, which usually starts with use, such as useRef, useState, useReducer, etc. Usually, when we write a React component, if the component is complex and has its own life cycle or state, we write it as a class component; If this component is only for presentation, write it as a function component.

React Hooks uses the writing method of function components to solve the problem that function components do not have a state through API s such as useState, solve the problem of life cycle through useEffect, and reuse business logic through custom hooks.

What problems does Hooks solve

  • Reusing the logic related to state extends the concept of HOC, but HOC will lead to the bloated component tree.
  • Solve the problem that components become difficult to maintain with business expansion.
  • Use function components that are easier to understand and more friendly to beginners.

usage

Hooks are mainly divided into three types:

  • State hooks: allows developers to use state in function components.
  • Effect hooks: allows developers to use life cycles and side effect s in function components.
  • Custom hooks: Custom hooks. You can use State Hooks and Effect Hooks to achieve logical reuse between components.

State Hooks

Take a look at the official demo

import { useState } from 'react';

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

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

The useState here is a hook, which returns an array. The first count represents a state, and the default value is 0; The second setCount is equivalent to the setState in class function and represents the update operation of count.

The advantage of this writing is that each state is managed independently to avoid state bloating when the state is complex.

The basic usage is described as follows:

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

useState returns an array. The first value is a stateful value, and the second value is a function that updates the state value. During initial rendering, the returned state is the same as initialState. During subsequent re rendering, the first value returned by useState will always be the latest state (state) after applying the update.

The setState function updates the state, accepts a new state value, and queues the component for re rendering.

Because setState uses a functional update method, you can pass a function to setState, which will receive the previous value and return the updated value.

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(0)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </>
  );
}

The above code can use the last state to calculate the new state. Unlike setState in the React class component, useState does not automatically merge and update objects. Therefore, if the state to be updated depends on the previous state, you need to use the object extension method:

setState(prevState => {
  // Object.assign is also feasible
  return {...prevState, ...updatedValues};
});

The initialState parameter can be either a value or a function. If the initial state is a high overhead calculation result, you can provide a function instead. This function is only executed during initial rendering:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

The initialState parameter is used only during the initial rendering and is ignored in subsequent renderings.

Effect Hooks

Effect Hooks allows side effects to be performed in components, similar to lifecycle methods in classes. Usually, we need to write some operations in componentDidMount and componentDidUpdate, which may be to update data or Dom. In addition, we will unbind some event monitoring during componentWillUnmount to prevent memory leakage. All these lead to the increase of component maintenance cost. In the function component, there are no these life cycles, so Hooks uses effect Hooks to replace these life cycles and complete some capabilities.

Take a look at the official demo of dynamically changing the title:

import { useState, useEffect } from 'react';

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

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

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

Before useEffect, we need to call the method of changing the title in componentDidMount and componentDidUpdate at the same time to complete the status of component initialization and data update. useEffect passes a function to React, and React calls this function after the component rendering is completed and updated to complete the above functions. By default, it runs after the first rendering and after each update.

useEffect Hook can be regarded as a combination of componentDidMount, componentDidUpdate and componentWillUnmount.

When does useEffect execute componentWillUnmount?

If a function is returned in useEffect, this function will be executed to clean up the effect when React uninstalls the current component.

Compare the two ways to write an effect that needs to be cleaned and an effect that does not need to be cleaned:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

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

  return (/*...*/);
}

Optimize performance by skipping Effect.

Usually, executing some logic every time a component is rendered or updated will cause unnecessary consumption, so we often write such code:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

The title is updated only when the state.count before and after the component update changes.

This problem can be handled more easily with Hooks

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

Pass the second parameter to useEffect, which is an array. If the component is re rendered, React will execute the contents of the function only when the count changes, otherwise it will directly skip the effect. If there are multiple parameters in the array, React will execute the contents of the function as long as one of them changes.

This also applies to effect s with cleanup phases:

useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes

If you want the effect to be executed only when the components componentDidMount and componentWillUnmount, you only need to pass an empty array to the second parameter. Pass in an empty array [] and input it to tell React that your effect does not depend on any value in the component, so the effect only runs at mount time and performs cleanup at unmount time, never at update time.

Hooks rules

In fact, React Hooks is not only an enhancement at the functional level, but also injects new software ideas into React. This is the popular "Convention is greater than configuration" in recent years. For example, the Hooks function must start with use, and the following rules. In front of my article What's new in webpack 4 Also mentioned this content.

Hooks is called only at the top level

Hooks can only be called at the top level, instead of calling Hook in loops, conditions or nested functions. The reason is that React needs to ensure that hooks are called in the same order every time the component is rendered.

If there are multiple Hooks in a component, how does React know which state corresponds to which useState call? The answer is that React depends on the order in which Hooks is called. Essentially, Hooks are arrays( React hooks: not magic, just arrays ). The subscript will change every time the useState is executed. If the useState is wrapped in a condition, the subscript may not be right every time, causing the useState to update the wrong data.

You can only call Hooks in React Function.

Hooks can only be invoked in React function components or invoked in custom Hooks. By following this rule, you can ensure that all stateful logic in a component is clearly visible in its source code.

eslint

eslint-plugin-react-hooks The enforcement of the above two rules can be guaranteed.

$ npm install [email protected]
// Your ESLint configuration
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error"
  }
}

Customize Hooks

Custom Hooks is to extract the common logic between components and write it as a separate function. The difference from general functions is that custom Hooks is a function starting with use, and other Hooks can be called internally.

import { useState, useEffect } from 'react';

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

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

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

  return isOnline;
}

export useFriendStatus;

In another component, after it is introduced, it can be used

import {useFriendStatus} from 'hooks/xxx.js';

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

As you can see, custom Hooks is just a JavaScript function, nothing special. However, it should be noted that the custom Hooks function must also start with use (protocol first).

useContext

const context = useContext(Context);

Accept a context object (the value returned from React.createContext) and return the current context value. When the provider updates, this Hook will trigger re rendering with the latest context value.

useReducer

const [state, dispatch] = useReducer(reducer, initialState);

useReducer can be understood as the Hooks of Redux. The first parameter accepted is the reducer of (state, action) = > new state, and returns the current state paired with the dispatch method.

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'reset':
      return initialState;
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      // A reducer must always return a valid state.
      // Alternatively you can throw an error if an invalid action is dispatched.
      return state;
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, {count: initialCount});
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'reset'})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

useReducer accepts the optional third parameter, initialAction, which indicates the action to be performed during component initialization. For example, use the value passed by props to initialize the state operation.

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'reset':
      return {count: action.payload};
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      // A reducer must always return a valid state.
      // Alternatively you can throw an error if an invalid action is dispatched.
      return state;
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(
    reducer,
    initialState,
    {type: 'reset', payload: initialCount},
  );

  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

useRef

const refContainer = useRef(initialValue);

useRef returns a mutable ref object, which is accessed through the. current attribute. The returned object will remain in the whole component life cycle.

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useImperativeMethods

useImperativeMethods(ref, createInstance, [inputs]);

useImperativeMethods are used in conjunction with forwardRef to represent mandatory methods. Expose a method of a child component to a parent component through ref.

Subcomponents:

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

Parent component:

function FancyParent() {
  const fancyInputRef = useRef(null);	
  useEffect(() => {
    fancyInputRef.current.focus(); 
  });

  return (
    <FancyInput ref={fancyInputRef} />
  );
}

useLayoutEffect

The usage is the same as useEffect, but it is triggered synchronously after all DOM changes. Use it to read the layout from the DOM and re render synchronously. useLayoutEffect refreshes synchronously before the browser draws.

The functions in useEffect are triggered after layout and paint. This makes it suitable for many common side effects, such as setting up subscriptions and event handlers, because most types of work should not prevent browsers from updating the screen.

However, if the effect cannot be delayed, such as DOM changes that must be triggered synchronously before the next drawing, it is more appropriate to use uselayouteeffect.

Hooks API

reference resources Hooks API Reference

summary

Hooks helps developers solve some logic reuse problems by setting some special functions to "hook" its life cycle and state inside the React component. By abstracting the code through custom hooks, we can write specifications that are more in line with functional programming, and reduce the problems caused by layers of nesting.

Reference documents

Posted on Fri, 26 Nov 2021 09:04:33 -0500 by dallasx