【React】ユースケースで理解するclean-up関数

clean-up関数は知識としては理解していたけど、使う場面に出会わなかったのでそれがなんなのかピンと来ていませんでした。

実際、clean-up関数は使う場面はそんなに多くないのでUsecasesを例に解説してみました。

# clean-up関数とは
useEffect内のCallbackが呼ばれる**前**にからなず実行される、クリーンアップフェーズ。クラスコンポーネントでのcomponentWillUnmount/componentDidUpdateに相当する。

# いつ実行されるのか

基本的には、**useEffectのcallback関数が実行される前**と理解してもらって問題ないと思います。

細かく言うと、v16とv17でclean-up関数が呼び出されるタイミングが違います。

あとコンポーネントがunmountするか、re-renderするかによっても挙動が異なりますが、ここでは深く触れないので**useEffectのcallback関数が実行される前**ということを理解してもらったらいいと思います。

*図は「初回render〜初回useEffect~DOMに描画するところまで」を省略しています。
*左の図がunmountされるコンポーネントで、右の図がre-renderされるコンポーネントの挙動です

### v16
![clean-up-v16.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1064297/8a41aeb1-1036-d579-9594-fbc251fd87c3.jpeg)

### v17
![clean-up-v17.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1064297/c8af3c8f-d899-ebd3-3edb-308a64692f01.jpeg)



# 何のために使うのか
## CASE1 : race conditionを回避するため
### race conditionとは

二つ以上のリクエストが送られた際、どちらかのリクエストが最初に完了するかによって、アプリケーションが異なる結果を表示する状態を指します。
q
例えば以下のような、`person`が更新されると非同期関数が実行されるコードがあるとしましょう。

```tsx
  const [data, setData] = React.useState<string | undefined>(undefined);
  const [person, setPerson] = React.useState<string | null>(null);
  const mountRef = React.useRef(false);

  React.useEffect(() => {
    if (mountRef.current) {
      const fakeFetch = () => {
        return new Promise<string>((res) => {
          setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
        });
      };
      fakeFetch().then((data) => setData(data));
    } else {
      mountRef.current = true;
    }
  }, [person]);

  return (
    <div className="App">
      <button onClick={() => setPerson("John")}>Request John's profile</button>
      <button onClick={() => setPerson("Ken")}>Request Ken's profile</button>
      <button onClick={() => setPerson("Michel")}>
        Request Michel's profile
      </button>
      <h1>{person}</h1>
      <h2>{JSON.stringify(data)}</h2>
    </div>
  );

```

`Request {name}'s profile` を短い間隔で２回以上クリックしてみます。

普通はrace conditionsはそう起きえないので、今回はリクエストごとに、ランダムな待ち時間を設定します。

![clean-up.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1064297/64d99289-ba55-da1c-e2aa-16c32e900e70.gif)


最後に`Michel's profile`をリクエストしたにも関わらず、`Ken's Data`が表示されてることが確認できます。

このようにリスポンンスが返される順序によってユーザーに表示される結果が変わることはuserにとって好ましくありません。

### 解決方法
主に二つあります。

1. 複数のリクエストを許容してもいい場合、booleanを使ってuserが最後にリクエストしたprofileのみsetStateするように制御する。
2. Internet Explorerをサポートする必要がない場合は、AbortControllerを使用してリクエストをキャンセルします

### boolean値を使った方法

次のコードを見てみましょう。

クリーンアップ関数の実行タイミングを思い出しながら、リクエストが短い間隔で2度行われた場合の挙動を確認してみましょう。

```tsx
const App = () => {
  const [data, setData] = React.useState<string | undefined>(undefined);
  const [person, setPerson] = React.useState<string | null>(null);

  React.useEffect(() => {
    let active = true;

    const fakeFetch = () => {
      return new Promise<string>((res) => {
        setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
      });
    };
    fakeFetch().then((fetchData) => {
      if (active) {　// activeがtrueの時のみsetDataできる
        console.log("set fetch data");
        setData(fetchData);
      }
    });

    return () => { // clean-up関数
      active = false;
    };
  }, [person]);

  return (
    <div className="App">
      <button onClick={() => setPerson("John")}>Request John's profile</button>
      <button onClick={() => setPerson("Ken")}>Request Ken's profile</button>
      <button onClick={() => setPerson("Michel")}>
        Request Michel's profile
      </button>
      <h1>{person}</h1>
      <h2>{JSON.stringify(data)}</h2>
    </div>
  );
};

```

1. `person state`が変更される
2.  useEffect[1]が実行されて`fakeFetch()[1]`が実行される
3. `fakeFetch()[1]`が完了する前に、`person state`が変更される
4.  useEffect[1]のclean-up関数が実行される
5. useEffect[2]が実行されて `fakeFetch()[2]`が実行される
6. useEffect[2]の結果が`setData()`され、re-renderが実行、DOMに反映され、画面に表示されます。

activeのboolean値をステップごとに確認してみましよう

1〜2の間は、`true`です。なのでuseEffect[1]の`fakeFetch()` **完了後**、`setData()`できます。

しかし、3によって再びrender関数が実行され、useEffect[1]の`fakeFetch()` 完了を待つ**前**にclean-up関数が実行され、`false`が代入されます。これにより、useEffect[1]の`fakeFetch()`は`setData()`を実行することができなくります。

5でuseEffect[2]の`fakeFetch()`が実行され、`true`なので結果を `setData()`することができます。

これにより、最後のリクエストのみ画面に表示することができました。

### AbortControllerを使った方法

次のコードを見てみましょう

```tsx
useEffect(() => {
  const abortController = new AbortController();
  const fetchData = async () => {
    setTimeout(async () => {
      try {
        const response = await fetch(`https://swapi.dev/api/people/${id}/`, {
          signal: abortController.signal,
        });
        const newData = await response.json();

        setFetchedId(id);
        setData(newData);
      } catch (error) {
        if (error.name === 'AbortError') {
          // Aborting a fetch throws an error
          // So we can't update state afterwards
        }
        // Handle other request errors here
      }
    }, Math.round(Math.random() * 12000));
  };

  fetchData();
  return () => {
    abortController.abort();
  };
}, [id]);

```

Boolean値を使った方法とやっていることはほぼ同じです。

異なる点は、リクエストをキャンセルしているのでこちらの方がユーザーにとってはエコですね。Internet Explorerを対応していないのであればこちらを使いたいですね。

Axiosを使っているのであれば、Cancel Tokenを使うこともできます


## CASE2 : サブスクリプションやタイマーを解除する
こちらの方が、Usecaseとしては馴染みがあるのではないでしょうか。

次のような、友達のオンランステータスをsubscriptionするコンポーネントがあります

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

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  });
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

```


このコードのままだと、コンポーネントがアンマウントした際におそらく次のようなwarningが表示されます

```markdown
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
```

要するに、「購読や非同期処理はアンマウント時に全てキャンセルしてください、じゃないとメモリリークします」ということです。

では、言われた通り実装してみましょう。

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

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {//追加↓
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };//追加↑
  });
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

```

Clean-up関数内で、購読を解除する処理を追加しました。

これでアンマウントしたコンポーネントのstateの更新を中止し、メモリリークせずにすみました。

次は、タイマーを解除しましょう。
やることは、先程のサブスクリプションと同様clean-up関数内で解除の処理を記載します。　

```tsx
import React, { useState, useEffect } from "react";

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

  useEffect(() => {
   const timeout = setTimeout(() => {
      setCount(1);
    }, 3000);

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

  return (
    <div className="App">
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

```

こちらも解除しないと、先程のwariningが表示されます。


# 最後に

他にUsecasesがあればコメントで教えてください🤲

# 参考資料
- [useEffectに非同期関数を設定する方法](https://zenn.dev/syu/articles/b97fb155137d1f)
- https://qiita.com/daishi/items/4423878a1cd7a0ab69eb
- [useEffect 内の非同期処理で local state を変更するときの注意点 | hbsnow.dev](https://hbsnow.dev/blog/react-async-useeffect-localstate/)
- https://takamints.hatenablog.jp/entry/cleanup-an-async-use-effect-hook-of-react-function-componet
- [React useEffect cleanup: How and when to use it - DEV Community](https://dev.to/otamnitram/react-useeffect-cleanup-how-and-when-to-use-it-2hbm)
- [Demystifying useEffect’s clean-up function — Max Rozen](https://maxrozen.com/demystifying-useeffect-cleanup-function)
- [Fixing Race Conditions in React with useEffect — Max Rozen](https://maxrozen.com/race-conditions-fetching-data-react-with-useeffect)
- [React初心者がReact Hooksの機能についてまとめてみた - ひろろの思うがままに。](https://hirolow.hatenablog.com/entry/hooks-intro)
- [副作用フックの利用法 – React](https://ja.reactjs.org/docs/hooks-effect.html#explanation-why-effects-run-on-each-update)
- [Aborting/Cancelling requests with Fetch or Axios | by Abhimanyu Chauhan | DataDrivenInvestor](https://medium.datadriveninvestor.com/aborting-cancelling-requests-with-fetch-or-axios-db2e93825a36)