useEffect内でsetTimeoutを使用する時の注意

## TL;DR
useEffect内でsetTimeoutを使用するときは、ちゃんとクリーンアップ処理を書きましょう。

```React
  useEffect(() => {
    let timeoutId = setTimeout(() => {
      // 5秒経過したら閉じる
      setIsOpen(false)
    }, 5000)
    // こういう処理を書く
+   return () => {
+     clearTimeout(timeoutId)
+   }
  }, [])

```

## クリーンアップ処理がないと起こりうるつらさ
コンポーネント状態が更新され、かつその状態がsetTimeoutの設定に依存している場合、意図しない動きをするケースがあるからです。


例えば数秒待機して自動で閉じるモーダルを実装するとします。
この場合、コンポーネントの実装としては以下のようになるかと思います。

```react
const Modal = () => {
  const [isOpen, setIsOpen] = useState(false)
  useEffect(() => {
    setTimeout(() => {
      // 5秒経過したら閉じる
      setIsOpen(false)
    }, 5000)
  }, [])
  
  return (
    <div>
      <p>モーダル</p>
    </div>
  )
}
```

ただコンポーネントの状態がこれのみならば、これで問題なく動いてくれると思います。
ここで、状態が更新されるモーダルを考えましょう。

```React
const modalReducer = (
  state: ModalKind,
  action: ModalKindAction
) => {
  switch (action.type) {
    case "info":
      return {
        type: action.type,
        message: "通知です"
      }
    case "success":
      return {
        type: action.type,
        message: "成功です"
      }
    case "error":
      return {
        type: action.type,
        message: "エラーです"
      }
    default:
      return state
  }
}

const VariousModal = () => {
  const [isOpen, setIsOpen] = useState(false)
  const [modalKind, dispatchModalKind] = useReducer(modalReducer, {
    type: "info",
    message: "通知です",
  })
  useEffect(() => {
    setTimeout(() => {
      // 5秒経過したら閉じる
      setIsOpen(false)
    }, 5000)
  }, [])
  
  return (
    <div>
      <h2>{modalKind.type}</h2>
      <p>{modalKind.message}</p>
    </div>
  )
}
```

```info```、```success```、```error``` の三つの状態を、利用シーンに応じて使い分けるような共通モーダルです。

この時点でも特に問題はないです。
では、エラーの場合のみ、モーダルを閉じない仕様を考えます。


```react
const VariousModal = () => {
  const [isOpen, setIsOpen] = useState(false)
  const [modalKind, dispatchModalKind] = useReducer(modalReducer, {
    type: "info",
    message: "通知です",
  })
  useEffect(() => {
    if (modalKind.type !== "error") {
      setTimeout(() => {
        // 5秒経過したら閉じる
        setIsOpen(false)
      }, 5000)
    }
  }, [])
  
  return (
    <div>
      <h2>{modalKind.type}</h2>
      <p>{modalKind.message}</p>
    </div>
  )
}
```
例えばデータフェッチなどで```info```状態のモーダルから、データが取得され次第 ```成功/失敗``` のモーダルに更新表示したい場合などに、5秒待たずして```error```状態になったとします。
エラー時にはモーダルが閉じてほしくないですが、上記のコードだとエラー状態にもかかわらず自動でモーダルが閉じる現象が発生します。

## なぜか？
setTimeoutのクリーンアップ処理を書いていないため、```info```状態で設定したsetTimeoutが生きてるからです。
以下のように書くと、コンポーネント更新時に、既存のsetTimeoutを削除してくれます。

```react
const VariousModal = () => {
  const [isOpen, setIsOpen] = useState(false)
  const [modalKind, dispatchModalKind] = useReducer(modalReducer, {
    type: "info",
    message: "通知です",
  })
  useEffect(() => {
    let timeoutId = null
    if (modalKind.type !== "error") {
      timeoutId = setTimeout(() => {
        // 5秒経過したら閉じる
        setIsOpen(false)
      }, 5000)
    }
    return () => {
      // クリーンアップ処理
      if (timeoutId) {
        clearTimeout(timeoutId)
      }
    }
  }, [])
  
  return (
    <div>
      <h2>{modalKind.type}</h2>
      <p>{modalKind.message}</p>
    </div>
  )
}
```

## まとめ
今回の例では種類に応じて切り替わるモーダルを例にしましたが、このケースはモーダルの種類毎にコンポーネントを別に作成することで回避することもできます。
あくまでsetTimeoutをuseEffectで使用する際に、クリーンアップ処理を書かないことで直感的でない動きをすることもあるということの一例として見てもらえれば幸いです。

特にコンポーネントの状態にsetTimeoutの実行が依存する場合などは、作法的にクリーンアップ処理を書いてしまっても良いのではないかと思いました。