React Hooks

What is the Hook

Hook is a new feature in React 16.8. It lets you use state and other React features without having to write a class.

Hook is a new feature introduced by the React team in React 16.8 that provides a more straightforward API for known React concepts while following functional components: The functions, state, Context, refs, and declaration cycle are designed to address the perennial problems with class components and make writing react components more efficient

Deficiencies of the class component

  • Difficult to reuse inter-component state logic: Reuse of component state logic requires solutions such as props Render and higher-order components, but the abstract encapsulation of such solutions leads to hierarchical redundancy, creating “nested hell”

  • Difficult to maintain complex components:

    • Many unrelated logic codes are intermixed in the same lifecycle, and related logic codes are split into different declaration cycles, which can easily be forgotten and lead to bugs
    • Components are often saturated with state logic access and processing and cannot be broken down into smaller granularity. State can be centrally managed through a state management library, but coupling the state management library leads to reduced component reusability
  • This points to the problem: In JavaScript, class methods do not bind this by default. This is undefined when a class method is called. To access this in a method, you must bind or use the class Fields syntax (experimental syntax) in the constructor.

    class Example extends React.Component {
    	constructor(props){...// Approach 1: bind this in the constructor
    		this.handleClick = this.handleClick.bind(this);
    	}
    	handleClick() {
    		this.setState({...})
    	}
    	
    	Method 2: Use the class fields syntax
    	handleClick = () = > {
    		this.setState({... }}})Copy the code
  • Difficult to compile and optimize for Class: Due to the historical design of JavaScript, using class components makes it difficult to optimize components during precompilation, such as classes that do not compress well, and hot overloading is unstable

The advantage of the Hook

  • Hooks allow you to reuse state logic without changing component structure (custom hooks)
  • Hooks break the interrelated parts of a component into smaller functions (such as setting up subscriptions or requesting data)
  • Hook allows you to use more React features in non-class situations

Hook usage rules

Hooks are Javascript functions, and there are two additional rules for using them:

  • A Hook can only be called outside a function, not inside a loop, a conditional judgment, or a child function
  • You can only call a Hook in the React function component and custom hooks. Do not call it from another JavaScript function

In the component React determines the useState corresponding to a certain state by judging the sequence of Hook calls. Therefore, it is necessary to ensure that the sequence of Hook calls is consistent between multiple renders so that React can correctly associate the internal state with the corresponding Hook

useState

UseState is used to add some internal state to a function component by calling it. Normally, pure functions do not have state side effects. This Hook function can be used to inject state into a function component

The only argument to useState is the initial state, which returns the current state and a status update function. The status update function returned by useState does not merge the new state with the old state, which can be manually merged using ES6 object structure syntax

const [state, setState] = useState(initialState);
Copy the code

Methods using

import React, { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={()= > setCount(count - 1)}>-</button>
      <input type="text" value={count} onChange={(e)= > setCount(e.target.value)} />
      <button onClick={()= > setCount(count + 1)}>+</button>
    </div>
  );
}
Copy the code

Equivalent class example

UseState returns a state similar to the class component defining this.state in its constructor, and returns a state update function similar to the class component’s this.setState

import React from 'react';

export default class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <button onClick={()= > this.setState({ count: this.state.count - 1 })}>-</button>
        <input type="text" value={this.state.count} onChange={(e)= > this.setState({ count: e.target.value })} />
        <button onClick={()= > this.setState({ count: this.state.count + 1 })}>+</button>
      </div>); }}Copy the code

Functional update

If the new state needs to be computed using the previous state, you can pass a function to setState, which will receive the previous state and return an updated value

import React, { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0);

  const lazyAdd = () = > {
    setTimeout(() = > {
      // Each execution will have the latest state instead of using the state when the event was triggered
      setCount(count= > count + 1);
    }, 3000);
  } 

  return (
    <div>
      <p>the count now is {count}</p>
      <button onClick={()= > setCount(count + 1)}>add</button>
      <button onClick={lazyAdd}>lazyAdd</button>
    </div>
  );
}
Copy the code

Lazy initial state

If the initial state needs to be computed through complex calculations, you can pass in a function that computes and returns the initial state, which will only be called during the initial render

import React, { useState } from 'react'

export default function Counter(props) {
  // The function is executed only once during the initial render and not again when the component is re-rendered
  const initCounter = () = > {
    console.log('initCounter');
    return { number: props.number };
  };
  const [counter, setCounter] = useState(initCounter);

  return (
    <div>
      <button onClick={()= > setCounter({ number: counter.number - 1 })}>-</button>
      <input type="text" value={counter.number} onChange={(e)= > setCounter({ number: e.target.value})} />
      <button onClick={()= > setCounter({ number: counter.number + 1 })}>+</button>
    </div>
  );
}
Copy the code

Skip state update

When you call the update function of the State Hook, React will use object. is to compare the two states. If true is returned, React will skip rendering and effect execution of the child components

import React, { useState } from 'react';

export default function Counter() {
  console.log('render Counter');
  const [counter, setCounter] = useState({
    name: 'timer'.number: 0
  });

  // Do not re-render if the state value passed does not change
  return (
    <div>
      <p>{counter.name}: {counter.number}</p>
      <button onClick={()= >setCounter({ ... counter, number: counter.number + 1})}>+</button>
      <button onClick={()= > setCounter(counter)}>++</button>
    </div>
  );
}
Copy the code

useEffect

Changing the DOM, adding subscriptions, setting timers, logging, and performing other side effects within the body of a function component (the React rendering phase) are not allowed, as this can cause unaccountable bugs and break UI consistency

UseEffect Hook is used to perform such side effects. UseEffect receives a function that contains imperative code that may have side effects

The useEffect function is executed after the browser has laid out and drawn, but before the next rendering, to ensure that the browser does not block updates to the screen

useEffect(didUpdate);
Copy the code

Methods using

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

export default function Counter() {
  const [count, setCount] = useState(0);

  // the useEffect callback is executed after the initial rendering and after the update
  // equivalent to componentDidMount and componentDidUpdate
  useEffect(() = > {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>count now is {count}</p>
      <button onClick={()= > setCount(count + 1)}>+</button>
    </div>
  );
}
Copy the code

Equivalent class example

UseEffect Hook execution time is similar to the componentDidMount and componentDidUpdate life cycle of the Class component, except that the function passed to useEffect is executed asynchronously after the browser has finished laying out and drawing

import React from 'react';

export default class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0}; }componentDidMount() {
    document.title = `You clicked The ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked The ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>count now is {this.state.count}</p>
        <button onClick={()= > this.setState({ count: this.state.count + 1 })}>+</button>
      </div>); }}Copy the code

Clear effect

The useEffect Hook function can return a cleanup function that will be executed before the component is uninstalled. The component executes this function to clear the previous effect before executing the next effect on multiple renderings

The cleanup function executes at a time similar to the Class component componentDidUnmount life cycle, in which useEffect is used to break the component’s interrelated parts into smaller functions, preventing forgetting from causing unnecessary memory leaks

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

export default function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() = > {
    console.log('start an interval timer')
    const timer = setInterval(() = > {
      setCount((count) = > count + 1);
    }, 1000);
    // Returns a cleanup function that is executed before the component is uninstalled and before the next effect execution
    return () = > {
      console.log('destroy effect');
      clearInterval(timer); }; } []);return (
    <div>
      <p>count now is {count}</p>
      <button onClick={()= > setCount(count + 1)}>+</button>
    </div>
  );
}
Copy the code

Optimize effect execution

By default, effect is executed after each component rendering is complete. UseEffect can take a second argument, which is the array of values on which effect depends, so that the subscription is recreated only if the array value changes. But here’s a caveat:

  • Ensure that the array contains all variables that change in the external scope and are used in effect
  • Passing an empty array as the second argument causes effect to be executed only once after the initial rendering
import React, { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() = > {
    document.title = `You clicked ${count} times`;
  }, [count]); // Update only when count changes

  return (
    <div>
      <p>count now is {count}</p>
      <button onClick={()= > setCount(count + 1)}>+</button>
    </div>
  );
}
Copy the code

useContext

Context provides a way to pass data between component trees without manually adding props for each layer of components. UseContext is used by function components to subscribe to changes in the upper-layer Context and obtain value prop values passed by the upper-layer Context

UseContext receives a context object (the return value of React. CreateContext) and returns the current value of the context, The current context value is determined by the < MyContext.provider > value prop of the upper-layer component closest to the current component

const value = useContext(MyContext);
Copy the code

Methods using

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

const themes = {
  light: {
    foreground: "# 000000".background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff".background: "# 222222"}};Create a context for the current theme
const ThemeContext = React.createContext();

export default function Toolbar(props) {
  const [theme, setTheme] = useState(themes.dark);

  const toggleTheme = () = > {
    setTheme(currentTheme= > (
      currentTheme === themes.dark
        ? themes.light
        : themes.dark
    ));
  };

  return (
    // Use the Provider to pass the current props. Value to the internal component
    <ThemeContext.Provider value={{theme, toggleTheme}} >
      <ThemeButton />
    </ThemeContext.Provider>
  );
}

function ThemeButton() {
  // Use useContext to get the current context value
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <button style={{background: theme.background.color: theme.foreground }} onClick={toggleTheme}>
      Change the button's theme
    </button>
  );
}
Copy the code

Equivalent class example

UseContext (MyContext) is equivalent to static contextType = MyContext or < myContext.consumer > in the class component

UseContext doesn’t change the way we consume the context, it just gives us an extra, prettier, prettier way to consume the upper context. This can be useful when applied to components that use multiple contexts

import React from 'react';

const themes = {
  light: {
    foreground: "# 000000".background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff".background: "# 222222"}};const ThemeContext = React.createContext(themes.light);

function ThemeButton() {
  return (
    <ThemeContext.Consumer>
      {
        ({theme, toggleTheme}) => (
          <button style={{background: theme.background.color: theme.foreground }} onClick={toggleTheme}>
            Change the button's theme
          </button>)}</ThemeContext.Consumer>
  );
}

export default class Toolbar extends React.Component {
  constructor(props) {
    super(props);
    
    this.state = {
      theme: themes.light
    };

    this.toggleTheme = this.toggleTheme.bind(this);
  }

  toggleTheme() {
    this.setState(state= > ({
      theme:
        state.theme === themes.dark
          ? themes.light
          : themes.dark
    }));
  }

  render() {
    return (
      <ThemeContext.Provider value={{ theme: this.state.theme.toggleTheme: this.toggleTheme}} >
        <ThemeButton />
      </ThemeContext.Provider>)}}Copy the code

Optimize the consumption context component

Components that call useContext are rerendered when the context value changes. To reduce the overhead of rerendering components, you can optimize them by using Memoization

Suppose for some reason you have AppContext, whose value has the Theme property, and you just want to re-render some ExpensiveTree on the AppContextValue.theme change

  1. Method 1: Split contexts that do not change together
function Button() {
  // Split the theme context out, other context changes will not cause ExpensiveTree to re-render
  let theme = useContext(ThemeContext);
  return <ExpensiveTree className={theme} />;
}
Copy the code
  1. When you cannot split the context, split the component in two and add to the middle componentReact.memo
function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Get the theme attribute
  return <ThemedButton theme={theme} />
}

const ThemedButton = memo(({ theme }) = > {
  // Use memo to reuse the last render as much as possible
  return <ExpensiveTree className={theme} />;
});
Copy the code
  1. Return a built-inuseMemoThe components of the
function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Get the theme attribute

  return useMemo(() = > {
    // The rest of your rendering logic
    return <ExpensiveTree className={theme} />;
  }, [theme])
}
Copy the code

useReducer

As an alternative to useState, useReducer is more suitable in some scenarios. For example, the state logic is complex and contains multiple subvalues, or the next state depends on the previous state.

Using useReducer also provides performance optimization for components that trigger deep updates because the parent component can pass dispatches to its own component instead of calling back functions

const [state, dispatch] = useReducer(reducer, initialArg, init);
Copy the code

Methods using

import React, { useReducer } from 'react'

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <p>Count: {state.count}</p>
      <button onClick={()= > dispatch({type: 'decrement'})}>-</button>
      <button onClick={()= > dispatch({type: 'increment'})}>+</button>
    </>
  );
}
Copy the code

Initialize the state

UseReducer initializes sate in two ways

1 / / way
const [state, dispatch] = useReducer(
	reducer,
  {count: initialCount}
);

2 / / way
function init(initialClunt) {
  return {count: initialClunt};
}

const [state, dispatch] = useReducer(reducer, initialCount, init);
Copy the code

useRef

The useRef is used to return a mutable ref object whose.current property is initialized as the passed parameter (initialValue).

The ref object created by useRef is a normal JavaScript object, while useRef() and its own {current:… The only difference with the} object is that useRef returns the same ref object every time it renders

const refContainer = useRef(initialValue);
Copy the code

Bind DOM elements

The ref object created with useRef can be used as a way to access the DOM by passing it into the component as

, React sets the.current property of the ref object to the corresponding DOM node after the component is created

import React, { useRef } from 'react'

export default function FocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () = > {
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
Copy the code

Bound variable value

The ref object created by useRef can also be used to bind any mutable value by manually setting the corresponding value to the object’s.current property

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

export default function Counter() {
  const [count, setCount] = useState(0);

  const currentCount = useRef();
  Use useEffect to get the current count
  useEffect(() = > {
    currentCount.current = count;
  }, [count]);

  const alertCount = () = > {
    setTimeout(() = > {
      alert(`Current count is: ${currentCount.current}, Real count is: ${count}`);
    }, 3000);
  }

  return (
    <>
      <p>count: {count}</p>
      <button onClick={()= > setCount(count + 1)}>Count add</button>
      <button onClick={alertCount}>Alert current Count</button>
    </>
  );
}
Copy the code

Performance Optimization (useCallback & useMemo)

The use of useCallback and useMemo in combination with the React.Memo method is a common performance optimization to avoid unnecessary re-rendering of child components due to parent component state changes

useCallback

UseCallback is used to create a callback function that returns a callback function that is updated only when a dependency changes. It is possible to pass the callback function to subcomponents that are optimized and use reference equality to avoid unnecessary rendering, in the same case with the props property, React skips rendering components and reuses the results of the last render

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

function SubmitButton(props) {
  const { onButtonClick, children } = props;
  console.log(`${children} updated`);

  return (
    <button onClick={onButtonClick}>{children}</button>
  );
}
// Use react. memo to check props changes and reuse the last render result
SubmitButton = React.memo(submitButton);

export default function CallbackForm() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const handleAdd1 = () = > {
    setCount1(count1 + 1);
  }

  // Calling useCallback returns an Memoized callback that is updated when the dependency is updated
  const handleAdd2 = useCallback(() = > {
    setCount2(count2 + 1);
  }, [count2]);

  return (
    <>
      <div>
        <p>count1: {count1}</p>
        <SubmitButton onButtonClick={handleAdd1}>button1</SubmitButton>
      </div>
      <div>
        <p>count2: {count2}</p>
        <SubmitButton onButtonClick={handleAdd2}>button2</SubmitButton>
      </div>
    </>)}Copy the code

UseCallback (fn, deps) equals useMemo(() => fn, deps).

const handleAdd2 = useMemo(() = > {
  return () = > setCount2(count2 + 1);
}, [count2]);
Copy the code

useMemo

You pass in the create function and the dependency array as arguments to useMemo, which recalculates memoized values only when a dependency changes. This optimization helps avoid costly calculations every time you render

Usage note:

  • The incominguseMemoDo not perform operations inside this function that are not related to rendering
  • If the dependency array is not provided,useMemoThe new value is computed on each render
import React, { useState, useMemo } from 'react';

function counterText({ countInfo }) {
  console.log(`${countInfo.name} updated`);

  return (
    <p>{countInfo.name}: {countInfo.number}</p>
  );
}
// // uses react. memo to check changes to the props and reuse the last render result
const CounterText = React.memo(counterText);

export default function Counter() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const countInfo1 = {
    name: 'count1'.number: count1
  };
  // Use useMemo to cache the result of the last calculation, which is recalculated only when the dependency changes
  const countInfo2 = useMemo(() = > ({
    name: 'count2'.number: count2
  }), [count2]);

  return (
    <>
      <div>
        <CounterText countInfo={countInfo1} />
        <button onClick={()= > setCount1(count1 + 1)}>Add count1</button>
      </div>
      <div>
        <CounterText countInfo={countInfo2} />
        <button onClick={()= > setCount2(count2 + 1)}>Add count2</button>
      </div>
    </>
  );
}
Copy the code

Other Hook

useImperativeHandle

UseImperativeHandle allows you to customize the instance value exposed to the parent component when using a ref. In most cases, imperative code like ref should be avoided. UseImperativeHandle should be used with the react. forwardRef:

import React, { useRef, useImperativeHandle, useState } from 'react'

function FancyInput(props, ref) {
  const inputRef = useRef();
  // Customize the ref instance value exposed to the parent component
  useImperativeHandle(ref, () = > ({
    focus: () = >{ inputRef.current.focus(); }}));return <input ref={inputRef} type="text" {. props} / >;
}
// Pass the exposed ref to the parent through the forwardRef
const ForwardFancyInput = React.forwardRef(FancyInput);

export default function Counter() {
  const [text, setText] = useState(' ');
  const inputRef = useRef();
  
  const onInputFocus = () = > {
    inputRef.current.focus();
  };

  return (
    <>
      <ForwardFancyInput ref={inputRef} value={text} onChange={e= > setText(e.target.value)} />
      <button onClick={onInputFocus}>Input focus</button>
    </>
  );
}
Copy the code

useLayoutEffect

UseLayoutEffect is similar to useEffect. Unlike useEffect, which executes effect asynchronously after the browser layout and painting are complete, useLayoutEffect is executed after the browser layout, Effect is executed synchronously before painting

The execution timing of useLayoutEffect is compared as follows:

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

export default function LayoutEffect() {
  const [width, setWidth] = useState('100px');

  UseEffect executes the effect callback after all DOM rendering is complete
  useEffect(() = > {
    console.log('effect width: ', width);
  });
  // useLayoutEffect executes effect callbacks synchronously after all DOM changes
  useLayoutEffect(() = > {
    console.log('layoutEffect width: ', width);
  });
  
  return (
    <>
      <div id='content' style={{ width.background: 'red' }}>content</div>
      <button onClick={()= > setWidth('100px')}>100px</button>
      <button onClick={()= > setWidth('200px')}>200px</button>
      <button onClick={()= > setWidth('300px')}>300px</button>
    </>
  );
}

// Use setTimeout to ensure execution after the component's first rendering completes and the corresponding DOM is retrieved
setTimeout(() = > {
  const contentEl = document.getElementById('content');
  // Monitor target DOM structure changes, called after useLayoutEffect callback and before useEffect callback
  const observer = new MutationObserver(() = > {
    console.log('content element layout updated');
  });
  observer.observe(contentEl, {
    attributes: true
  });
}, 1000);
Copy the code

Customize the Hook

Before the Hook feature, React had two popular ways to share state logic between components: render props and higher-order components, but such solutions led to problems such as hierarchical redundancy in the component tree. The use of custom Hook can be a good solution to such problems

Create custom hooks

A custom Hook is a function whose name starts with “use” that can call other hooks from within. Here is a custom Hook implementation to get the mouse position in real time:

import { useEffect, useState } from "react"

export const useMouse = () = > {
  const [position, setPosition] = useState({
    x: null.y: null
  });

  useEffect(() = > {
    const moveHandler = (e) = > {
      setPosition({
        x: e.screenX,
        y: e.screenY
      });
    };

    document.addEventListener('mousemove', moveHandler);
    return () = > {
      document.removeEventListener('mousemove', moveHandler); }; } []);return position;
}
Copy the code

Use custom hooks

The rules for using custom hooks are basically the same as the rules for using hooks. The following is the process for using custom hooks:

import React from 'react';
import { useMouse } from '.. /hooks/useMouse';

export default function MouseMove() {
  const { x, y } = useMouse();
  return (
    <>
      <p>Move mouse to see changes</p>
      <p>x position: {x}</p>
      <p>y position: {y}</p>
    </>
  );
}
Copy the code

React executes this function to obtain a separate state and execute a separate side function each time a custom Hook is used. All states and side effects are completely isolated

reference

React Hooks official documentation

React useCallback & useMemo

Preventing rerenders with React.memo and useContext hook

MutationObserver MDN

Difference between useLayoutEffect and useEffect