React Hooks are new in React 16.8. They allow you to use state and other React features without writing classes.

First let’s look at what code written using Hooks looks like:

import React, { useState, useEffect } from'the react;export default() = > {const [count, setCount] = useState(0);

  useEffect((a)= > {
    document.title = count;
  });

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

This code implements a simple counter function that increases count by clicking a button and synchronizes the display of the page title with the change in count. Here we introduce useState and useEffect. We’ll talk about them later. Let’s compare them with class Component:

import React, { Component } from'the react;export default class extends Component {
  state = {
    count: 0}; componentDidMount() {document.title = this.state.count;
  }

  componentDidUpdate() {
    document.title = this.state.count;
  }

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

By contrast, it is much simpler to write code using Hooks, the way state is initialized and changed has changed, lifecycle functions are no longer needed, Hooks give us the ability to use state and lifecycle in function components. React’s component classification has also been redefined, with stateful and stateless components now divided into class and function components.

So let’s look at the specific uses of Hooks.

useState

First up is useState, what is this for? UseState allows us to useState within function components for state management, with the following syntax:

// State and setState can be named arbitrarily, such as count, setCount
const [state, setState] = useState(initialState)
Copy the code

UseState takes an initialState, initialState, and it returns an array that it deconstructs, state, and setState, the function that updates state, and every time setState is called, A re-rendering of the component is enqueued (re-rendering the component or batch updating it with other setStates).

InitialState is the initialState. It can be a basic type, an object (such as {a:1}), or a function. It is used only for the first rendering of the component, after which state is the same as initialState.

UseState returns the latest state after each rerender. SetState is like this.setState() in the class component, but setState in the Hooks does not merge old state with new state, it overwrites it.

UseState () also takes arbitrary arguments, either primitives or objects, and passes in a function that takes the old state, for example:

{/* <button onClick={() => setCount(count + 1)}>increment count</button> */}
<button onClick={() => setCount((oldCount) => oldCount + 1)}>
  increment count
</button>
Copy the code

UseState can also be used multiple times for multiple state management:

const [count, setCount] = useState(0);
const [color, setColor] = useState('red');
Copy the code

useEffect

So what is a useEffect? UseEffect allows us to perform side effects in function components, such as fetching data, DOM manipulation, logging, and so on.

UseEffect can be seen as a combination of three life cycle functions componentDidMount, componentDidUpdate, componentWillUnmount.

For example, in the previous example we used useEffect to update the page title:

useEffect((a)= > {
  document.title = count;
});
Copy the code

UseEffect is executed every time setCount is used to update the state. This is the same as using componentDidMount and componentDidUpdate at the same time:

componentDidMount() {
  document.title = this.state.count;
}
componentDidUpdate() {
  document.title = this.state.count;
}
Copy the code

As you can see, the same logic is written in both life cycles, which undoubtedly makes the code redundant, whereas useEffect is executed after every rendering, including the first rendering, so we can just write the same operation as in the example and put it in useEffect once.

The side effects in the above example do not need to be removed, but there are also side effects that need to be removed, such as manually bound DOM events.

Clearing side effects in class components can be done in the componentWillUnmount lifecycle method, so how do you do that in Hooks?

UseEffect already provides a cleanup mechanism. For each side effect, you can add an optional return function that cleans up when the component is uninstalled, as in the following code:

import React, { useState, useEffect } from'the react;export default() = > {const [size, setSize] = useState({ width: 0.height: 0 });

  const handleResize = (a)= > {
    const win = document.documentElement.getBoundingClientRect();
    setSize({ width: win.width, height: win.height });
  };

  useEffect((a)= > {
    window.addEventListener('resize', handleResize);
    return (a)= > {
      window.removeEventListener('resize', handleResize);
    };
  });

  return (
    <div>
      <p>Current size is:</p>
      <p>
        width: {size.width}, height: {size.height}
      </p>
    </div>
  );
};
Copy the code

Another problem with using useEffect is that every time a setState operation is performed, a side effect may be triggered, even if the changed state is unrelated to the side effect, as in the following code:

import React, { useState, useEffect } from'the react;export default() = > {const [count, setCount] = useState(0);
  const [color, setColor] = useState('red');

  useEffect((a)= > {
    console.log('count');
    document.title = count;
  });

  return (
    <div>
      <p style={{ color: color}} >Current count is: {count}</p>
      <button onClick={()= > setCount(count + 1)}>increment count</button>
      <br />
      <button onClick={()= >SetColor (color = = = "red"? 'blue' : 'red')}> Switch color</button>
    </div>
  );
};
Copy the code

We found that the side effect of changing the page title was repeated while changing the color, which was a waste of resources and not exactly what we wanted. UseEffect, of course, gives us a second argument that controls rendering. It takes an array as an argument and can pass in multiple values. Let’s improve the code above:

useEffect((a)= > {
  console.log('count');
  document.title = count;
}, [count]);
Copy the code

We pass [count] as the second argument, so that the new value of count is compared with the old value on each rendering, and this effect is only performed if the value changes.

In addition to using Hooks, follow these rules:

  • Use Hooks only in function components, not in normal functions
  • Use Hooks only at the top level; do not use Hooks in conditions, loops, or nested functions

Of course, to ensure these rules, we can use the ESLint plugin eslint-plugin-react-hooks to implement constraints.

useContext

First, let’s look at how Context works: Context provides a way to pass data across layers, eliminating the need to manually pass props at each layer of the component.

Context API usage example

To review the use of the Context API, use the following example:

// Create a Context object
constThemeContext = React. CreateContext (" light ");const ContextAPIDemo = (a)= > {
  return (
    // Each Context object returns a Provider React component,
    // It receives a value attribute, which is passed to the consuming component,
    // Allow consuming components to subscribe to changes to the context
    <ThemeContext.Provider value="dark"> <MiddleComponent /> </ThemeContext.Provider> ); }; Const MiddleComponent = () => {return (<div> <ThemedButton /> </div>); }; Class ThemedButton extends Component {// Assigns the ThemeContext object to the contextType static property. Static contextType = ThemeContext; static contextType = ThemeContext; render() { return <Button theme={this.context} />; }} const Button = (props) => {const btnBgColor = props. Theme === = 'dark'? "# 333" : "white"; return <button style={{ backgroundColor: btnBgColor }}>Toggle Theme</button>; };Copy the code

Here we create a ThemeContext using react.createcontext () and pass the data to the consumer component via a Provider, ThemedButton. Finally, in the consuming component, we assign the ThemeContext we created to the contextType static property, so that we can access the data in the consuming component through this.context.

You can also use the Consumer component to retrieve Context data:

const ThemedButton = (a)= > {
  return (
    <ThemeContext.Consumer>
      {(theme) => <Button theme={theme} />}
    </ThemeContext.Consumer>
  );
};
Copy the code

Ok, with a brief review, let’s see how Context is used in Hooks.

The use of useContext

Let’s modify the above example with useContext:

// omit other code... const Button = () => { const theme = useContext(ThemeContext); Const btnBgColor = theme === 'dark'? "# 333" : "white"; return <button style={{ backgroundColor: btnBgColor }}>Toggle Theme</button>; };Copy the code

With this modification, the Consumer or contextType static property is not needed at all, and useContext is used wherever it is needed.

UseContext receives a Context object as a parameter and returns the value data passed through the Provider.

Since there is always a fixed ThemeContet in the example, what if you need a dynamic Context? We can use the Provider’s value to pass in the callback function to handle it:

const ContextAPIDemo = (a)= > {
  const [theme, setTheme] = useState(initialState.theme);

  const toggleTheme = (val) = > {
    setTheme(val === 'dark' ? 'light' : 'dark');
  };

  return (
    <ThemeContext.Provider value={{ theme.toggleTheme}} >
      <MiddleComponent />
    </ThemeContext.Provider>
  );
};
Copy the code

You can then get the callback function in the target component via useContext, for example:

const Button = () => {
  const { theme, toggleTheme } = useContext(ThemeContext);
  const btnBgColor = theme === ‘dark’ ? ‘#333’ : ‘white’;
  return (
    <button
      style={{ backgroundColor: btnBgColor }}
      onClick={() => toggleTheme(theme)}
    >
      Toggle Theme
    </button>
  );
};
Copy the code

useReducer

Let’s talk about Reducer, those who have used Redux should be familiar with Reducer. Reducer in Reducer Hook actually means the same as Reducer in Redux, receiving old state and returning new state, as follows: (state, action) => newState.

The basic syntax of useReducer looks like this:

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

It receives three parameters: Reducer, initialArg and an optional init function:

  • reducer: receives the old state and returns the newState, such as: ‘(state, action) => newState
  • initialArgInitial state:
  • init: passed as a function, lazily initializes state, which will be set toinit(initialArg)This can take the logic for calculating the state out of the system, and can be handy if there is a need to reset the state

Here’s how to use it:

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();
  }
}

const Counter = (a)= > {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={()= > dispatch({ type: 'decrement' })}>-</button>
      <button onClick={()= >Dispatch ({type: "increment"})} > +</button>
    </>
  );
};
Copy the code

useMemo

The syntax of useMemo looks like this:

const memoizedValue = useMemo((a)= > computeExpensiveValue(a, b), [a, b]);
Copy the code

Taking a function and an array of dependencies as parameters, and then evaluating the value of memoizedValue only when the dependencies change, we can use this as a means of performance optimization.

For example, in the following code, we add useMemo to the computedNum method and use count as a dependency. The computedNum will be computed only when count changes. This prevents irrelevant operations from causing the function to execute. ComputedNum is also computed repeatedly when you click change Color).

export default() = > {const [count, setCount] = useState(0);
  const[color, setColor] = useState (" blue ");const computedNum = useMemo((a)= > {
    console.log('render when count change');
    return count + 1;
  }, [count]);

  return (
    <div>
      <p>Current color is: {color}</p>
      <p>Current count is: {count}</p>
      <p>Computed num is: {computedNum}</p>
      <button onClick={()= > setCount(count + 1)}>increment count</button>
      <button onClick={()= >SetColor (color = = = "blue"? 'green' : 'blue')}> change color</button>
    </div>
  );
};
Copy the code

Of course, we can also wrap a component with useMemo to achieve a pureComponent-like effect and prevent the component from being rendered repeatedly.

<>
  {useMemo(
    (a)= > (
      <ColorDemo color={color} />
    ),
    [color]
  )}
<>
Copy the code

useCallback

UseCallback is used as follows:

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

UseCallback returns a memoized callback function, which is similar to useMemo, useCallback(fn, deps) equals useMemo(() => fn, deps). Its second argument is the same as useMemo’s, passing in an array of dependencies. New functions are generated only when the dependencies change, otherwise the same function is always generated. This is very useful when tuning component performance to avoid unnecessary overhead. For example, when passing functions using props, using callback avoids generating a new function each time you render, as shown in the following example:

let firstOnClick;
const Button = (props) = > {
  if(! firstOnClick) { firstOnClick = props.onClick; }console.log(firstOnClick === props.onClick);
  return <button onClick={props.onClick}>increment count</button>;
};

export default() = > {const [count, setCount] = useState(0);
  const [color, setColor] = useState('blue');

  // Without using the useCallback wrapper, click the Change color Button and a new function will be generated each time in the Button component
  // const handleIncrementCount = () => {
  // setCount(count + 1);
  // };

  Increment Count = increment Count; increment Count = increment Count;
  // Changing the color does not generate new functions
  const handleIncrementCount = useCallback((a)= > {
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <p>Current color is: {color}</p>
      <p>Current count is: {count}</p>
      <Button onClick={handleIncrementCount} />
      <button onClick={()= > setColor(color === 'blue' ? 'green' : 'blue')}>
        change color
      </button>
    </div>
  );
};
Copy the code

useRef

UseRef returns a mutable ref object whose.current property is initialized as the passed parameter (initialValue), and the returned REF object remains unchanged for the lifetime of the component.

Typically it is used to get child components (or DOM elements) :

export default() = > {const btnRef = useRef(null);

  useEffect((a)= > {
    btnRef.current.addEventListener('click', () => {
      alert('click me'); }); } []);return (
    <div>
      <button ref={btnRef}>click</button>
    </div>
  );
};
Copy the code

In addition, since useRef returns the same ref object every time it renders, it is possible to store a variable with ref.current that will not be affected by the component’s re-rendering, as in the following timer example:

export default() = > {const [count, setCount] = useState(0);
  const ref = useRef(0);

  useEffect((a)= > {
    const timer = setInterval((a)= > {
      console.log('interval');
      setCount(++ref.current);
    }, 1000);

    return (a)= >clearInterval(timer); } []);return (
    <div>
      <p>Current count is: {count}</p>
    </div>
  );
};
Copy the code

Also, changing the.current property does not cause the component to be rerendered.

useImperativeHandle

UseImperativeHandle allows you to customize the value of an instance exposed to the parent component when using a ref. This allows you to hide private methods or properties from the forwardRef.

function Input(props, ref) {
  const [val, setVal] = useState(0);

  useEffect((a)= > {
    setVal(props.count);
  }, [props.count]);

  const clearInput = useCallback((a)= > {
    setVal(' '); } []); useImperativeHandle(ref, () => ({clear: (a)= >{ clearInput(); }}));return( <input type="text" value={val} onChange={(e) => setVal(e.target.value)} /> ); } const FancyInput = forwardRef(Input); export default () => { const [count, setCount] = useState(0); const fancyRef = useRef(null); const handleClearInput = useCallback(() => { fancyRef.current.clear(); } []); return ( <div> <p>Current count is: {count}</p> <button onClick={() => setCount(count + 1)}>increment count</button> <hr /> <FancyInput ref={fancyRef} count={count} /> <button onClick={handleClearInput}>clear input</button> </div> ); };Copy the code

useLayoutEffect

UseLayoutEffect is used in the same way as useEffect, except that useLayoutEffect is called synchronously after all DOM updates. You can use useLayoutEffect to read the DOM layout and trigger the rerender synchronously, but useEffect is recommended. To avoid blocking UI updates.

Here’s an example where using useEffect and useLayoutEffect makes a significant difference:

const useLayoutEffectDemo = (a)= > {
  const [height, setHeight] = useState(100);
  const boxRef = useRef(null);

  useLayoutEffect((a)= > {
    if (boxRef.current.getBoundingClientRect().height < 200) {
      console.log('set height: ', height);
      setHeight(height + 10);
    }
  }, [height]);

  const style = {
    width: '200px'.height: `${height}px`.backgroundColor: height < 200 ? 'red' : 'blue'};return (
    <div ref={boxRef} style={style}>
      useLayoutEffect Demo
    </div>
  );
};
Copy the code

We are basically giving the box a rough transition. Using useEffect this transition works, but with useLayoutEffect the transition is gone and only the final effect is displayed. This means that before the browser performs the drawing, the update plan inside useLayoutEffect will be refreshed synchronously.

useDebugValue

For developing custom Hooks debugging use, for example:

useDebugValue(
  size.width < 500 ? '---- size.width < 500' : '---- size.width > 500'
);
Copy the code

You can view the output in React DevTools.

Custom Hooks

React Hooks are powerful, not only because of the official built-in Hooks, but also because of the support for custom Hooks, which improve component reusability and, to some extent, replace HOC and Render props.

A custom Hook needs to start with “use” and other hooks can be called internally. A custom Hook does not need to have a special identifier. We can customize parameters and return values.

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

const getDomElInfo = (a)= > document.documentElement.getBoundingClientRect();

const getSize = (a)= > ({
  width: getDomElInfo().width,
  height: getDomElInfo().height,
});

export default function useSize() {
  const [size, setSize] = useState((a)= > getSize());

  const onChange = useCallback((a)= > {
    setSize((a)= >getSize()); } []); useEffect((a)= > {
    window.addEventListener('resize', onChange, false);

    return (a)= > {
      window.removeEventListener('resize', onChange, false);
    };
  }, [onChange]);

  return size;
}
Copy the code

Using this custom Hook is also simple:

import useSize from './useSize';

export default() = > {const size = useSize();
  return (
    <div>
      <p>
        size: width-{size.width}, height-{size.height}
      </p>
    </div>
  );
};
Copy the code

That’s it for React Hooks, which not only break the way components are written, but are a new way of thinking that is well worth learning.

Relevant reference

  • Introducing Hooks – React
  • Example using useLayoutEffect – CodeSandbox
  • 17 – useLayoutEffect (React HOOKS) – YouTube