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>
)
}
info
、success
、error
の三つの状態を、利用シーンに応じて使い分けるような共通モーダルです。
この時点でも特に問題はないです。
では、エラーの場合のみ、モーダルを閉じない仕様を考えます。
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の実行が依存する場合などは、作法的にクリーンアップ処理を書いてしまっても良いのではないかと思いました。