iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🧪

Two Types of "Side Effects" in React

に公開
3

Hello everyone. When talking about React, the term side effect comes up frequently. However, in reality, I suspect we are actually using the term "side effect" in two different senses, which might be causing confusion.

For example, in a recent article I wrote, I explained that basically, you should not fetch inside useEffect.

https://qiita.com/uhyo/items/dec319ced85fc1b83f86

Regarding such topics, criticisms like "What's wrong with fetching inside useEffect?" are sometimes received[1]. In other words, the opinion is that since useEffect is a hook for describing side effects and fetch is a network request (a side effect), it should be appropriate to fetch inside useEffect.

In my view, this is a misunderstanding caused by conflating two types of "side effects."

In short, the "side effects" mentioned in the following two sentences actually refer to slightly different, distinct concepts (though they are not entirely unrelated).

  • useEffect is a hook for describing component side effects.
  • In React, components should be kept pure and side effects should not be written directly inside the component.

The “Side Effect” of useEffect

First, let's examine the first “side effect”.

useEffect is a hook for describing component side effects.

This is a concept I also explained in the previously mentioned Qiita article. First, let's consider what a component's effect (action) is. Here, I am referring to what happens when that component is rendered as its effect.

The primary effect of a component is to display the component's content (the specified DOM elements) at the position where the component is rendered. A component declares its own effect by returning JSX[2], and the React runtime actually executes that effect.

However, depending on the component, there may be impacts (effects) resulting from the rendering other than displaying in the DOM. In the Qiita article mentioned earlier, I avoided the term "side effect" and explained it as "an additional effect of the component being displayed," but if we consider it as something that happens beyond the primary effect, calling it a side effect does not seem entirely wrong.

An example of a "component side effect" in this sense would be registering an event handler for a DOM element outside the component's jurisdiction (such as the entire screen).

useEffect(() => {
  const controller = new AbortController();
  document.addEventListener("scroll", () => {
    // Do something 
  }, {
    passive: true,
    signal: controller.signal,
  });

  return () => {
    controller.abort();
  };
}, []);

And the important thing to note about component side effects is that they are ultimately a part of the component's effect. In other words, even if it is a side effect, it should not deviate from the principles of a component.

The principles of a component here mainly refer to the following two things:

  1. A component's effect is only valid while the component is mounted. This means that when a component is unmounted, the side effect also disappears. The example above follows this principle because the event handler is removed when the component unmounts.
  2. A component's effect is determined by pure calculation using the component's props and state as input. This means that not only must the primary effect, "the displayed content of the component," be determined based on the component's props and state, but the same must apply to the component's side effects. That is, the content of the side effect also needs to be determined based on the component's props and state.

The “side effect” defined in useEffect is not a total lawless zone; it is an API used to define a part of a component's effect, so it must be implemented according to these rules.

useEffect is also sometimes explained as an effect with a lifecycle. This also aligns with the idea that the side effect of useEffect is part of the component's effect. Just as the component's content can change (the primary effect changes) if the component's props or state change, the side effect of useEffect can also change depending on props and state. In order to implement those changes correctly, the effect needs to trigger firing and cleanup. To learn more about this perspective, I recommend the following article.

https://zenn.dev/yumemi_inc/articles/react-effect-simply-explained

Side Effects as a General Programming Concept

The second “side effect” refers to side effects as a term in general programming, not a concept unique to React.

In React, components should be kept pure, and side effects should not be written directly inside the component.

Simply put, a function with side effects is not a pure function. Conversely, side effects are what make a function impure. Specifically, a side effect refers to any effect caused externally other than the return value obtained as a result of the function. A network request being made is a typical side effect in this sense.

Terms like "pure function" often appear in React rules and design theories. Functional components are said to be pure functions (although they are special in that they call hooks[3]). Also, when performing state updates in useState with a function, that function is also expected to be a pure function.

As this is a common concept in the context of React but not specific to React, I will not explain it in detail in this article.

The Relationship Between the Two Side Effects

Here, let's call the "side effect of useEffect" Side Effect ①, and the "side effect in the sense of not being a pure function" Side Effect ②.

Side Effect ① and Side Effect ② are similar yet distinct concepts, but they are not unrelated. In the official React documentation linked at the beginning, there is the following description:

https://react.dev/learn/keeping-components-pure#where-you-can-cause-side-effects

While functional programming relies heavily on purity, at some point, somewhere, something has to change. That’s the whole point of programming! These changes—updating the screen, starting an animation, changing the data—are called side effects. They’re things that happen “on the side”, not during rendering.

In React, side effects usually belong inside event handlers. (...)

If you’ve exhausted all other options and can’t find the right event handler for your side effect, you can still attach it to your returned JSX with a useEffect call in your component. This tells React to execute it later, after rendering, when side effects are allowed. However, this approach should be your last resort.

According to this, describing side effects using useEffect is introduced as a last resort.

In the terminology of this article, this description refers to using useEffect as a means to trigger Side Effect ②. In other words, useEffect, which is originally a tool for Side Effect ①, can also be used for Side Effect ② (as a last resort). In this regard, the two side effects seem not to be unrelated (and at the same time, this is likely the cause of the confusion pointed out at the beginning of this article).

However, as evident from the fact that this is considered a last resort, it is not a practice that should be used regularly. In cases where you "use useEffect to trigger some Side Effect ② during rendering," it is likely not the correct use of useEffect. This is because the effects of that Side Effect ② are unlikely to align with the component's lifecycle. Correct usage, as mentioned above, is to use it to implement Side Effect ① as part of the component's responsibility.

Summary

When people say "useEffect is a hook for side effects," my view is that this "side effect" refers to Side Effect ① ("component side effects") and not Side Effect ②.

The belief that you can cause any Side Effect ② inside useEffect is likely a misunderstanding caused by equating these two types of side effects. useEffect is strictly a hook for Side Effect ①; using it for Side Effect ② should be avoided as much as possible and should only be a last resort when there are no other options.

I hope this article helps you organize your thoughts.

Aside

In this article, I took the stance of calling useEffect a "side effect" and explained how the concept of side effects is split into two categories as a result. However, I also believe that if we didn't call useEffect a side effect to begin with, this confusion wouldn't occur. Therefore, to be honest, I am not a fan of calling useEffect a "side effect." While I acknowledge that it is not necessarily wrong to call it a side effect as explained in the main text, I think it might be easier to understand if it weren't called that.

To start with, the "effect" in the name useEffect simply means "action" or "effect," and it is not called a "side effect," right? Logic written in useEffect is also part of the component's effect. Considering useEffect as an API for defining a component's effect, I personally prefer to simply call the logic written in useEffect an "effect."

You might think that what I'm saying here contradicts the main text, but I wrote this article from the perspective of those who call useEffect a "side effect" to make it easier to communicate with them. In this aside, I have explained my original stance.

脚注
  1. In the aforementioned article, for some reason it disappeared, but I saw a comment with such phrasing on Hatena Bookmark. ↩︎

  2. Since JSX is a syntax, strictly speaking, it returns the value of a JSX expression. In terms of TypeScript types, this is a ReactElement. ↩︎

  3. If we consider the return values of hooks as inputs to the function, it might not be impossible to apply the concept of pure functions. However, it still deviates from the general computation model of functions in that the calculations within the function are used for inputs to hooks. ↩︎

GitHubで編集を提案

Discussion

あいや - aiya000あいや - aiya000

本来副作用①のための道具であるuseEffectを、副作用②のために(最終手段として)使うこともできてしまうのです

なるほど。コンポーネント外の真の副作用①である

eff :: A -> IO B

があったときに

somePureComponent :: MonadState State m => B -> m (FC X)

があって、その内部で

-- `MonadReader B m`は「useEffectはBを含むクロージャーにできるよ」の気持ち
useEffect :: (MonadState State m, MonadReader B m) => m ()

が使われるべきなのに、実際は

useEffect :: IO () -- これが②

になっている(※JavaScriptなのでB -> m (FC X)内でIO ()が呼べる)
という話か〜

NakamuraNakamura

何が①の副作用で、何が②の副作用になるのかは、これまた論争になりそうですね

FF

長年のモヤモヤがスッキリしました...!

Reactではコンポーネントは純粋に保つべきであり、コンポーネント内に直接副作用を記述してはいけない。

ただこの箇所について少しひっかかりました。
私の認識としてはReactはコンポーネントの"render処理"は純粋に保つべきであり、コンポーネント全体では
「UI = f(state)」以外の処理も担当するのでコンポーネント全体では純粋ではない気がしました、、、😵‍💫