useEffectのCleanupの使い方まとめ
かれこれ2年くらいはReactを使っていたのですが、今までuseEffectのCleanupを意識したことがなかったので、Cleanupの使い方をまとめてみようと思います。
ただし、意識してこなかったからには現場の使い方など知らないので…公式ドキュメントをCleanupに注目して読み、その内容をまとめました。
state/prop依存の非同期処理をする
state/propに依存した非同期処理をする場合、適切にcleanupを設定して、prop/stateが変化していないことを確認する必要があります。
- NG
const [id, setId] = useState('hoge');
const [res, setRes] = useState<Response | undefined>();
useEffect(() => {
fetch(`https://someurl/${id}`).then((response) => {
setRes(response);
});
}, [id]);
- OK
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
useEffect(() => {
useInterval(() => {
// do something;
}, 1000);
}, []);
- OK
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
useEffect(() => {
const handler = () => { alert('hoge'); };
window.addEventListener('onclick', handler);
}, []);
- OK
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だけでどうにかなるものなのか、若干怪しい気もしますが…
※次のコードと説明は、参考元のコードで十分わかりやすいため、そのまま引用します。
useEffect(() => {
ChatAPI.subscribeToSocket(props.userId);
return () => ChatAPI.unsubscribeFromSocket(props.userId);
}, [props.userId]);
上記の例では、userId が 3 から 4 に変わった場合、ChatAPI.unsubscribeFromSocket(3) が最初に走り、その後に ChatAPI.subscribeToSocket(4) が走ります。クリーンアップ関数は「前回」の userId をクロージャとしてキャプチャしていますので、前回の値を取得する必要はありません。
※ドキュメントにも少し残っているように、以前は usePrevious のカスタムフックが書いてありましたが、今ではできるだけ使わないようにするべきなんですね…
最後に
最後まで読んでいただき、ありがとうございました。
今まで意識してこなかった方(主に筆者)は、これを機会に上記のケースだけでもcleanupを適切に書いてみてはいかがでしょうか。
もしここにないプラクティスがあれば、ぜひコメントで教えてください!