useEffectのCleanupの使い方まとめ

# useEffectのCleanupの使い方まとめ

かれこれ2年くらいはReactを使っていたのですが、今までuseEffectのCleanupを意識したことがなかったので、Cleanupの使い方をまとめてみようと思います。
ただし、意識してこなかったからには現場の使い方など知らないので…公式ドキュメントをCleanupに注目して読み、その内容をまとめました。

## state/prop依存の非同期処理をする

参考：<https://ja.reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies>

state/propに依存した非同期処理をする場合、適切にcleanupを設定して、prop/stateが変化していないことを確認する必要があります。

- NG

```typescript
  const [id, setId] = useState('hoge');
  const [res, setRes] = useState<Response | undefined>();
  useEffect(() => {
    fetch(`https://someurl/${id}`).then((response) => {
      setRes(response);
    });
  }, [id]);
```

- OK

```typescript
  const [id, setId] = useState('hoge');
  const [res, setRes] = useState<Response | undefined>();
  useEffect(() => {
    let ignore = false;
    fetch(`https://someurl/${id}`).then((response) => {
      if (!ignore) setRes(response);
    });
    return () => { ignore = true };
  }, [id]);
```

cleanupを使用してignoreフラグを確認すれば、次のようなタイミング問題を回避できます。

- (1)id='hoge'でfetchを開始する。
- (2)id='fuga'に変わり、id='fuga'でfetchを開始する。
- (3)先にid='fuga'のfetchが完了
- (4)後にid='hoge'のfetchが終了
  - →idは'fuga'なのに、最後のsetResはid='hoge'に対する物がセットされてしまう。

※CodePenでちょっとしたデモを作りました。<https://codepen.io/charon1212/pen/WNzWypB>

## setIntervalやsetTimeoutを使う

参考：<https://ja.reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often>

こちらも非同期処理みたいなものですが、setIntervalのようなAPIを使うときには、対応するclear関数でcleanup処理を書きます。

- NG

```typescript
  useEffect(() => {
    useInterval(() => {
      // do something;
    }, 1000);
  }, []);
```

- OK

```typescript
  useEffect(() => {
    const id = useInterval(() => {
      // do something;
    }, 1000);
    return () => {
      clearInterval(id);
    }
  }, []);
```

上記はsetIntervalについての例ですが、setTimeoutも同じようにできるようです。

## イベントリスナーを追加する

参考：<https://ja.reactjs.org/docs/hooks-faq.html#should-i-use-one-or-many-state-variables>

addEventListenerでリスナーを登録するときは、対応するremoveEventListenerをcleanupで書きます。

- NG

```typescript
useEffect(() => {
  const handler = () => { alert('hoge'); };
  window.addEventListener('onclick', handler);
}, []);
```

- OK

```typescript
useEffect(() => {
  const handler = () => { alert('hoge'); };
  window.addEventListener('onclick', handler);
  return () => window.removeEventListener('onclick', handler);
}, []);
```

趣味でreactを使うときはフロント全体をReactで書いてしまうので、EventListenerをいじるような使い方はしないのですが…
いざ使う時が来たら、ちゃんとcleanupしましょう！

## ひとつ前のstate/propに関する処理を書く

参考：<https://ja.reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state>

こういった要件に出くわしたことは無いのですが、cleanup関数の中でstate/propを書けば、疑似的に1個前のstate/propが使えます。
実際に出くわした場合、cleanupだけでどうにかなるものなのか、若干怪しい気もしますが…

※次のコードと説明は、参考元のコードで十分わかりやすいため、そのまま引用します。

```typescript
  useEffect(() => {
    ChatAPI.subscribeToSocket(props.userId);
    return () => ChatAPI.unsubscribeFromSocket(props.userId);
  }, [props.userId]);
```

> 上記の例では、userId が 3 から 4 に変わった場合、ChatAPI.unsubscribeFromSocket(3) が最初に走り、その後に ChatAPI.subscribeToSocket(4) が走ります。クリーンアップ関数は「前回」の userId をクロージャとしてキャプチャしていますので、前回の値を取得する必要はありません。

※ドキュメントにも少し残っているように、以前は usePrevious のカスタムフックが書いてありましたが、今ではできるだけ使わないようにするべきなんですね…

## 最後に

最後まで読んでいただき、ありがとうございました。
今まで意識してこなかった方（主に筆者）は、これを機会に上記のケースだけでもcleanupを適切に書いてみてはいかがでしょうか。
もしここにないプラクティスがあれば、ぜひコメントで教えてください！