3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

TL;DR

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

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

クリーンアップ処理がないと起こりうるつらさ

コンポーネント状態が更新され、かつその状態がsetTimeoutの設定に依存している場合、意図しない動きをするケースがあるからです。

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

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

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

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>
  )
}

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

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

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を削除してくれます。

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の実行が依存する場合などは、作法的にクリーンアップ処理を書いてしまっても良いのではないかと思いました。

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?