With the new API for hooks added to React in v16.8.0, it is important to understand how to use it and be able to write a few custom hooks to suit our business.

1. Some common hooks

Several built-in hooks are officially provided, so let’s take a brief look at their usage.

1.1 useState: state hooks

Data that needs to be updated on the page state can be put into the useState hook. For example, if you click a button, the data is incremented by 1.

1
2
3
4
5
6
7
8
const [count, setCount] = useState(0);

return (
  <>
    <p>{count}</p>
    <button onClick={() => setCount(count + 1)}> add 1 </button>
  </>
);

In the typescript system, the type of count is, by default, the type of the current initial value, e.g. the variable in the above example is of type number. If we want to customise the type of this variable, we can define it after useState as follows.

1
const [count, setCount] = (useState < number) | (null > null); // Variable count is of type number or null

Also, when using useState to change state, the entire state is replaced, so if the state variable is an object type of data and I only want to change one of the fields, it will automatically merge the data internally when setState is called from within the previous class component.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Home extends React.Component {
  state = {
    name: "wenzi",
    age: 20,
    score: 89,
  };

  update() {
    this.setState({
      score: 98,
    }); // Automatic internal merging
  }
}

However, when using useState within a function component, you need to merge the data yourself before calling the method, otherwise the fields will be lost.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const [person, setPerson] = useState({
    name: 'wenzi',
    age: 20,
    score: 89
});

setPerson({
    ...person,
    {
        score: 98
    }
}); // Merge data first { name: 'wenzi', age: 20, score: 98 }
setPerson({
    score: 98
}); // Only the fields to be modified are passed in, after which the name and age fields are lost

1.2 useEffect: side effect hooks

useEffect can be thought of as a combination of the functions componentDidMount, componentDidUpdate and componentWillUnmount.

The useEffect hook must be executed once when the component has been initialized. Whether or not the component is updated during re-rendering depends on the 2nd parameter passed in.

  1. when there is only one argument to the callback function, the callback is executed for each update of the component.
  2. when there are 2 parameters, the callback is executed only if the data in the 2nd parameter changes.
  3. to be executed only once when the component has been initialised, the 2nd parameter can be passed into an empty array.

We can see in this example that the useEffect callback will be executed whether the add button or the settime button is clicked.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const Home = () => {
  const [count, setCount] = useState(0);
  const [nowtime, setNowtime] = useState(0);

  useEffect(() => {
    console.log("count", count);
    console.log("nowtime", nowtime);
  });

  return (
    <>
      <p>count: {count} </p>
      <p>nowtime: {nowtime} </p>
      <button onClick={() => setCount(count + 1)}> add 1 </button>
      <button onClick={() => setNowtime(Date.now())}> set now time </button>
    </>
  );
};

If this is changed, the callback will only be output on the console when the count changes, and not when the nowtime value is changed.

1
2
3
4
useEffect(() => {
  console.log("count", count);
  console.log("nowtime", nowtime);
}, [count]);

The useEffect callback function can also return a function which is called before the end of the effect’s life cycle. To prevent memory leaks, the clear function is executed before the component is unloaded. Also, if the component is rendered multiple times, the previous effect is cleared before the next effect is executed.

Based on the above code, we modify it slightly.

1
2
3
4
5
6
useEffect(() => {
  console.log("count", count);
  console.log("nowtime", nowtime);

  return () => console.log("effect callback will be cleared");
}, [count]);

react useEffect

This mechanism is particularly useful in cases where there is a need to add and remove bindings, such as listening for changes in the window size of a page, setting timers, establishing and disconnecting from a back-end websocket service, and so on. The useEffect can be wrapped again to form a custom hook, which we will talk about below.

1.3 useMemo and useCallback

The variables and methods defined in the function component are re-calculated when the component is re-rendered, as in the following example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const Home = () => {
  const [count, setCount] = useState(0);
  const [nowtime, setNowtime] = useState(0);

  const getSum = () => {
    const sum = ((1 + count) * count) / 2;
    return sum + " , " + Math.random(); // This random is to see the difference
  };

  return (
    <>
      <p> count: {count}</p>
      <p> sum: {getSum()}</p>
      <p> nowtime: {nowtime}</p>
      <button onClick={() => setCount(count + 1)}> add 1 </button>
      <button onClick={() => setNowtime(Date.now())}> set now time </button>
    </>
  );
};

There are 2 buttons, one for count+1 and one to set the current timestamp. The getSun() method calculates the sum from 1 to count, and the sum method will recalculate the sum each time we click the add button. However, when we click the settime button, the getSum method will also recalculate, which is unnecessary.

Here we can use useMemo to modify it.

1
2
3
4
5
6
const sum = useMemo(
  () => ((1 + count) * count) / 2 + " , " + Math.random(),
  [count]
);

<p> {sum} </p>;

Once modified, you can see that the sum value is only recalculated when the count changes, and is not recalculated when the settime button is clicked. This is thanks to the useMemo hook feature.

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

useMemo returns the value returned in the callback, and memoizedValue it is only recalculated when a dependency changes. This optimisation helps to avoid a high overhead calculation at each rendering. If an array of dependencies is not provided, useMemo would calculate the new value each time it is rendered.

In the example above, sum is only recalculated when the count variable changes, otherwise the value of sum remains the same.

useCallback is the same type as useMemo, except that useCallback returns a function, e.g.

1
2
3
const fn = useCallback(() => {
  return ((1 + count) * count) / 2 + " , " + nowtime;
}, [count]);

2. Implementing a few custom hooks

In the official documentation, the online and offline functionality for friends is implemented. Here we learn to implement a few hooks ourselves.

2.1 Getting the changing width and height of a window

We get the width and height of the window in real time by listening to the resize event, and wrapping this method to automatically unbind the resize event before the end of its life.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const useWinResize = () => {
  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight,
  });
  const resize = useCallback(() => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight,
    });
  }, []);
  useEffect(() => {
    window.addEventListener("resize", resize);
    return () => window.removeEventListener("resize", resize);
  }, []);
  return size;
};

It is also very easy to use.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const Home = () => {
  const { width, height } = useWinResize();

  return (
    <div>
      <p>width: {width}</p>
      <p>height: {height}</p>
    </div>
  );
};

2.2 Timer useInterval

When using a timer in a front-end, it is usually necessary to clear the timer before the end of the component’s lifecycle, and if the timer’s period has changed, to clear the timer before restarting it according to the new period. The most common scenario for this is a nine-box draw, where the user clicks to start the draw, starts slowly, then gradually gets faster, the interface returns the winning result, then starts to slow down and finally stops.

It is easy to think of using useEffect to implement such a hook (as an example of an error).

1
2
3
4
5
6
7
8
const useInterval = (callback, delay) => {
  useEffect(() => {
    if (delay !== null) {
      let id = setInterval(callback, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
};

Let’s try this code in our project.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const Home = () => {
  const [count, setCount] = useState(0);

  useInterval(() => {
    console.log(count);
    setCount(count + 1);
  }, 500);

  return <div> {count} </div>;
};

However, this runs strangely, the page goes from 0 to 1 and then never stays the same. The output of console.log(count) indicates that the code is not stuck, so what’s the problem?

The props and states in React components can change, and React re-renders them and “discards” any results from the last rendering, so they are no longer relevant to each other.

The useEffect() Hook also ‘discards’ the previous rendering result, it clears the previous effect and creates the next effect, which locks in the new props and state, which is why our first attempt at a simple example worked correctly.

But setInterval doesn’t ’throw away’. It will keep referring to the old props and state until you replace it - you can’t do that without resetting the time.

The hook useRef is used here. We store the callback in the ref and update the value of ref.current when the callback is updated.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const useInterval = (callback, delay) => {
  const saveCallback = useRef();

  useEffect(() => {
    // Save a new callback to our ref after each rendering
    saveCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      saveCallback.current();
    }
    if (delay !== null) {
      const id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
};

When we use the new useInterval, we find that self-incrementing is possible.

Here we use a variable to control the rate of increase.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const [count, setCount] = useState(0);
const [diff, setDiff] = useState(500);

useInterval(() => {
  setCount(count + 1);
}, diff);

return (
  <div>
    <p> count: {count} </p>
    <p> diff: {diff}ms </p>
    <p>
      <button onClick={() => setDiff(diff - 50)}> 50ms faster </button>
      <button onClick={() => setDiff(diff + 50)}> 50ms slowdown </button>
    </p>
  </div>
);

The rate at which the count is increased can be adjusted by clicking on the two buttons separately. When you want to stop the timer, set diff to null (setDiff(null)). When it is reset to a number, the timer restarts.

3. Summary

There are a lot of interesting things you can do with the react hook, but here are just a few simple examples. We’ll get more into how hooks work later on.