Recently I’ve come to realize that React hooks are very useful and I’ve reacquainted myself with the React framework. Here’s how to understand hooks properly, and to analyze useEffect(), one of the most important hooks, in depth. The content will be as common as possible, so that friends who are not familiar with React can also read.

React’s two sets of APIs

Previously, there was only one set of React API, but now there are two: the class API and the function-based hooks API.

react

Any component that can be written in a class or a hook. Here is how the class is written.

1
2
3
4
5
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

Look again at the way hooks are written, that is, functions.

1
2
3
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

These two ways of writing serve exactly the same purpose. It’s natural for beginners to ask, “Which API set should I use?”

The official recommendation is to use hooks (functions) rather than classes. Because hooks are cleaner, less code-intensive, and “lighter” to use, while classes are “heavier”. Also, hooks are functions, which is more in line with the functional nature of React.

Here is a comparison of the amount of code in the class component (left) and the function component (right). For complex components, the difference is much more.

However, hooks are so flexible that they are not very easy for beginners to understand. Many people have a half-understanding and can easily write confusing and unmaintainable code. Then it would be better to use classes. Because classes have a lot of mandatory syntax constraints and are not easy to mess up.

The difference between classes and functions

Strictly speaking, there is a difference between class components and function components. The different ways of writing them represent different programming methodologies.

A class is an encapsulation of data and logic. That is, the state and operation methods of the component are encapsulated together. If you choose to write a class, you should write all the relevant data and operations in the same class.

Functions in general should only do one thing, which is to return a value. If you have multiple operations, each operation should be written as a separate function. Also, the state of the data should be separated from the operation method. According to this philosophy, React’s function component should do only one thing: return the component’s HTML code, and nothing else.

Still using the function component above as an example.

1
2
3
4

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

This function does only one thing, it returns the HTML code of the component, based on the input parameters. This kind of function that only performs pure data computation (conversion) is called a “pure function” within functional programming.

What are the side effects?

When you see this, you may have a question: If pure functions can only perform data computation, where should those operations that do not involve computation (such as generating logs, storing data, changing application state, etc.) be written?

Functional programming refers to operations that are not related to data computation as “side effects”. If a function contains operations that produce side effects directly inside the function, it is no longer a pure function, and we call it an impure function.

Pure functions can contain side effects internally only by indirect means (i.e., through other function calls).

The role of hooks

Having said all that, what exactly are hooks?

In a nutshell, a hook is a side effect solution for React function components that is used to introduce side effects to the function component. The body of the function component should only be used to return the component’s HTML code, all other operations (side effects) must be introduced via hooks.

Since there are so many side effects, there are many kinds of hooks, and React provides dedicated hooks for many common operations (side effects).

  • useState(): save state
  • useContext(): save context
  • useRef(): save reference
  • ……

All of these hooks introduce a specific side effect, while useEffect() is the generic side effect hook. If you can’t find a corresponding hook, you can use it. In fact, as you can see from the name, it is directly related to side effect.

Usage of useEffect()

useEffect() itself is a function, provided by the React framework, that can be called from within the function component.

For example, we want the page title (document.title) to change when the component is loaded. Then, the action of changing the page title is a side effect of the component and must be implemented with useEffect().

1
2
3
4
5
6
7
8
import React, { useEffect } from 'react';

function Welcome(props) {
  useEffect(() => {
    document.title = 'Loading completed';
  });
  return <h1>Hello, {props.name}</h1>;
}

In the above example, the argument to useEffect() is a function that is the side effect (changing the page title) to be accomplished. Once the component is loaded, React will execute this function. (See the results of the run)

What useEffect() does is to specify a side effect function that is automatically executed once for every rendering of the component. The side effect function is also executed after the component is loaded for the first time in the web page DOM.

The second parameter of useEffect()

Sometimes we don’t want useEffect() to be executed every time we render, in which case we can use its second argument to specify the dependencies of the side effect function using an array, and only if the dependencies change will they be re-rendered.

1
2
3
4
5
6
7

function Welcome(props) {
  useEffect(() => {
    document.title = `Hello, ${props.name}`;
  }, [props.name]);
  return <h1>Hello, {props.name}</h1>;
}

In the above example, the second argument to useEffect() is an array specifying the dependencies (props.name) of the first argument (the side effect function). The side effect function will be executed only if this variable changes.

If the second argument is an empty array, it means that the side effect parameter does not have any dependencies. Therefore, the side effect function will only be executed once at this point after the component is loaded into the DOM, and not again when the component is re-rendered later. This makes sense, because the side effect does not depend on any variables, so the result of the side effect function will not change no matter how those variables change, so running it once is enough.

Uses of useEffect()

Any side effect can be introduced using useEffect() as long as it is a side effect. It has the following common uses.

  • fetching data (data fetching)
  • Event listening or subscription (setting up a subscription)
  • Changing the DOM (changing the DOM)
  • Exporting logs (logging)

The following is an example of getting data from a remote server. (View the results of the run)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'https://hn.algolia.com/api/v1/search?query=redux',
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

In the above example, useState() is used to generate a state variable (data) to store the fetched data; the side effect function of useEffect() has an async function inside to fetch the data from the server asynchronously. After getting the data, setData() is used to trigger the re-rendering of the component.

The second argument of useEffect() in the above example is an empty array, since fetching data only needs to be executed once.

The return value of useEffect()

Side effects occur as the component is loaded, then it may be necessary to clean up these side effects when the component is unloaded.

useEffect() allows to return a function that is executed when the component is unloaded to clean up the side effects. If there is no need to clean up side effects, useEffect() does not need to return any value.

1
2
3
4
5
6
useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    subscription.unsubscribe();
  };
}, [props.source]);

In the above example, useEffect() subscribes to an event when the component is loaded and returns a cleanup function that is unsubscribed when the component is unloaded.

In practice, since the side effect function is executed every time it is rendered by default, the cleanup function will not only be executed once when the component is unloaded, but also once every time before the side effect function is re-executed to clean up the side effects of the last rendering.

Notes on useEffect()

There is one thing to note when using useEffect(). If you have multiple side effects, you should call multiple useEffect()s and should not combine them and write them together.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function App() {
  const [varA, setVarA] = useState(0);
  const [varB, setVarB] = useState(0);
  useEffect(() => {
    const timeoutA = setTimeout(() => setVarA(varA + 1), 1000);
    const timeoutB = setTimeout(() => setVarB(varB + 2), 2000);

    return () => {
      clearTimeout(timeoutA);
      clearTimeout(timeoutB);
    };
  }, [varA, varB]);

  return <span>{varA}, {varB}</span>;
}

The above example is the wrong way to write it, there are two timers inside the side effect function, they are not related to each other, in fact they are two unrelated side effects and should not be written together. The correct way to write them is to write them separately as two useEffect().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function App() {
  const [varA, setVarA] = useState(0);
  const [varB, setVarB] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(() => setVarA(varA + 1), 1000);
    return () => clearTimeout(timeout);
  }, [varA]);

  useEffect(() => {
    const timeout = setTimeout(() => setVarB(varB + 2), 2000);

    return () => clearTimeout(timeout);
  }, [varB]);

  return <span>{varA}, {varB}</span>;
}

Reference https://www.ruanyifeng.com/blog/2020/09/react-hooks-useeffect-tutorial.html