ReactのuseEffectにおけるクリーンアップの重要性
はじめに
クリーンアップとは、useEffect
フック内で設定したリソースを後始末(クリーンアップ)することである。
React のuseEffect
フックは、コンポーネントがマウントされたときやアンマウントされたとき、または指定された値が変更されたときに副作用(データの取得、購読、DOM の変更など)を実行するために使用される。
非常に便利なフックであるが、クリーンアップ処理を行わないと、メモリリークや不要なリクエストが発生するなど、予期せぬバグやパフォーマンス問題を引き起こす場合がある。
実際に私も、useEffect
のクリーンアップ処理を行わなかったことで、バグにハマってしまった経験がある。その経験も踏まえて、useEffect
におけるクリーンアップの重要性についてまとめてみた。
クリーンアップを行わないとどうなるか
下記のコードを例に、useEffect
のクリーンアップを行わない場合の問題点について説明する。
import React, { useEffect } from "react";
const TimerComponent = () => {
let seconds = 0;
useEffect(() => {
const interval = setInterval(() => {
seconds++;
console.log(seconds);
}, 1000);
}, []);
return <div>Seconds: {seconds}</div>;
};
export default TimerComponent;
このコードは、TimerComponent
コンポーネントがマウントされたときに、1 秒ごとにseconds
をインクリメントしてコンソールに出力する処理を行う。
一見すると問題なさそうに見えるが、実際にこのコードを実行してみると、以下のような問題が発生する。
1 秒ごとに 2 ずつ増えていることがわかる。
では、次にuseEffect
の処理が何回実行されているかを確認してみる。
import React, { useEffect } from "react";
const TimerComponent = () => {
+ console.log("TimerComponent rendered");
let seconds = 0;
useEffect(() => {
const interval = setInterval(() => {
seconds++;
console.log(seconds);
}, 1000);
}, []);
return <div>Seconds: {seconds}</div>;
};
export default TimerComponent;
2 回実行(レンダリング)されていることがわかる。
useEffect
が 2 回実行されているので、setInterval
も 2 回実行されていることがわかる。
そのため、interval
も 2 つ作成されており、1 秒ごとに 2 ずつ増えているのである。
クリーンアップ処理を行う
useEffect
のクリーンアップ処理を行うためには、useEffect
のコールバック関数内でクリーンアップ処理を行うための関数を返す必要がある。
下記にクリーンアップ処理を追加したコードを記述する。
import React, { useEffect } from "react";
const TimerComponent = () => {
console.log("TimerComponent rendered");
let seconds = 0;
useEffect(() => {
const interval = setInterval(() => {
seconds++;
console.log(seconds);
}, 1000);
+ return () => {
+ clearInterval(interval);
+ };
}, []);
return <div>Seconds: {seconds}</div>;
};
export default TimerComponent;
return
で返された関数は、useEffect
が再実行される直前に実行される。
つまり、新しいinterval
が作成される前に、古いinterval
がクリーンアップされる。
このコードを実行すると、useEffect
は 2 回実行されているが、interval
は 1 つしか作成されないため、正しく動作することがわかる。
経験したバグ
実際に私が経験したバグについて記述する。
あるプロジェクトで、useEffect
の中でform
の値を非同期でバリデーションする処理を実装した。
※form
は、Ant Design
のForm
コンポーネントを使用した。
useEffect(() => {
... // 他の処理
form
.validateFields()
.then((values) => {
// エラーがなかった場合の処理
})
.catch((error) => {
// エラーがあった場合の処理
});
}, [form, ...]);
しかし、このコードでは正しく動作しなかった。
form
の値に問題がないにも関わらず、エラーが発生してしまうのである。
error
をコンソールに出力してみると、error.outOfDate
というプロパティがtrue
になっていることがわかった。
outOfDate
とは、Ant Design
のForm
コンポーネントにおいて、非同期でバリデーションを行っている際に、form
の値が変更された場合に発生するエラーである。
今回の場合、form
の値は変更されていないが、useEffect
の他の依存値が変更されたことで、useEffect
が再実行され、form
のバリデーション処理が再実行されてしまい、outOfDate
エラーが発生していた。
この問題を解決するためには、useEffect
のクリーンアップ処理を行う必要があった。
修正後のコードはクリーンアップの説明後に記述する。
クリーンアップ処理を行った後のコード
isCurrent
という変数を使用して、非同期処理中にuseEffect
が再実行された場合は、isCurrent
がfalse
になるようにし、非同期処理完了後の処理はisCurrent
がtrue
の場合のみ実行するようにした。
このようにすることで複数回実行された場合は、最後に実行された非同期処理の結果のみを反映することができる。
useEffect(() => {
+ let isCurrent = true;
... // 他の処理
form
.validateFields()
.then((values) => {
+ if (isCurrent) {
// エラーがなかった場合の処理
+ }
})
.catch((error) => {
+ if (isCurrent) {
// エラーがあった場合の処理
+ }
});
+ return () => {
+ isCurrent = false;
+ };
}, [form, ...]);
まとめ
useEffect
のクリーンアップ処理を行わないと、メモリリークや不要なリクエストが発生するなど、予期せぬバグやパフォーマンス問題を引き起こす場合がある。
useEffect
内で実行しているコードが複数回実行されても問題がないか確認し、問題がある場合はクリーンアップ処理を行うことが重要である。
Discussion