【React修行日記】useEffectと依存配列
学習の目的
- useEffectの基本的な使い方を理解する
- 依存配列の役割と正しい使い方を理解する
useEffectとは
useEffectはReactで使用するフックの一つで、Reactコンポーネントが「描画された後」に処理を行う。
コンポーネントの副作用を制御する機能。コンポーネントのマウント時(一番最初)にも必ず実行される。
import {useEffect} from "react";
useEffect(() => {
// statement:描画時に実行すべき処理
}, [/* dependencies:依存配列 */]);
- 第1引数: 実行する関数
- 第2引数: 依存配列
「副作用」とは
Reactで言う「副作用(side effect)」は、画面の描画(UIを返す処理)以外の処理を指す。
レンダリングの中に混ぜると動作が予測しづらくなるため、useEffect で分離して管理する。
例えば...
- localStorage にデータを保存する
- DOMを直接操作する
- イベントリスナーを登録する
依存配列の役割
依存配列を指定することで、その場合だけ処理を実行することができる。
副作用の処理内で参照されている、レンダリングによって変化する可能性があるstateやpropsなどのリアクティブな値は全て依存配列に入っている必要がある。
つまり、自分で勝手に依存配列を決めて実行タイミングを制御できるものではない。
公式でも以下のように言及されている。
エフェクトの依存配列は自分で「選ぶ」たぐいのものではないことに注意してください。エフェクトのコードで使用されるすべてのリアクティブな値は、依存値のリスト内で宣言されなければなりません。依存配列は、その周囲にあるコードによって決定されます。
依存配列に正しい値が入っていない場合バグの要因となり、exhaustive-depsというルールの元リンタエラーが発生する。
これは決して無視して良いものではなく、正しく依存配列を指定する必要がある。
useEffectを使用すべき状況とは
前回作成したカウンターを利用して、カウントアップされたらアラートを表示するようにしてみた。
import { useEffect, useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount((count) => count + 1);
};
useEffect(() => {
if (count > 0) {
alert(`カウントが${count}になりました`);
}
}, [count]);
return (
<div className="flex items-center flex-col gap-5">
<p className="text-2xl font-bold">{count}</p>
<div className="flex gap-4">
<button onClick={increment}>+1</button>
</div>
</div>
);
}
依存配列にcountが指定されていることで、カウントが変わるたびにアラートが表示される。
しかし、これはuseEffectを使用しなくてもカウントアップのボタンがクリックされた時のincrement関数にalert(`カウントが${count}になりました`);を含めれば同じ挙動を実装できる。
const increment = () => {
setCount((count) => count + 1);
alert(`カウントが${count + 1}になりました`);
};
// useEffectは削除
ユーザイベントの処理にエフェクトは必要ありません。
と公式でも触れられている通り、useEffectを利用すべき状況は限られている。
useEffectを使用するのは以下のような、「Reactの外側」と連携する状況の時。
- 外部システムと同期するとき
- 例: DOMを手動で操作する、外部APIにサブスクライブする、外部ライブラリを初期化する
- 非表示状態でも続く処理が必要なとき
- 例: チャット接続を維持する、監視を続ける、計測を行う
- Reactの描画結果を超えて「外部の世界」に影響を与えるとき
- 例: ドキュメントタイトルを変更する、ログを送信する、タイマーをセットする
無限ループに注意
useEffect の依存配列を誤ると、エフェクトが永遠に再実行される「無限ループ」が発生する。
例えば以下のような場合に発生する(他にもあるはず)
- エフェクト内で setState を呼び、その state を依存配列に入れている
- エフェクトで使う関数が毎回再生成され、依存することでエフェクトが毎レンダリング再実行される
悪い例:
// ❌ NG(無限ループの例)
const [count, setCount] = useState(0);
useEffect(() => {
// count を変更しているのに依存に count を入れている → ループ
setCount(count + 1);
}, [count]);
①setCountがcountを変える → ②再レンダリング → ③countが変わったのでeffectが再実行 → ④永久ループ。
そもそも上記の例ではuseEffectを使うべきか、やはりしっかりと検討する必要がある。
まとめ
- useEffectは外部システムと同期する時に使用する
- useEffectを使用するときは本当に必要か慎重に判断する
- 依存配列を正しく指定しないと無限ループが発生する可能性あり
- useEffectについては実装経験を通してもう少し理解を深めたい
参考
Discussion
失礼します。
「ボタンを押下するとフェッチ」「初期レンダリング時にもフェッチ」「そのためには無限ループが生じてしまうので仕方なく依存配列から
fetchDataを除く。そのために exhaustive-deps ルールを破る」というのは、useEffect の使い方の説明として適切でないと思います。
そもそも、useEffect の設計意図は「発火タイミングは依存配列でコントロール」するように考えられていないからです。
「useEffect の使い方」という趣旨からはズレますが、その画面を実現するためには、useEffect ではなく TanStack Query とその refetch 機能で実現するのが良いと思います。
(React 公式ドキュメントにおいても、API のデータフェッチは useEffect からではなく、フレームワークの機能や TanStack Query を使うことが推奨されています。)
動作確認はできていませんが、こんな感じで書けるはずです。
ご教授いただきありがとうございます...!
私の知識不足とlintの設定不備で誤った理解をしておりました🙇
以下のような理解で合っておりますでしょうか?
これは OK です。
ここまでも OK です。
しかし、「データフェッチで useEffect ではなくライブラリを使用すべき」なのは、それとは別の問題です。無限ループが起きる場合でも起きない場合でも関係なく、「データフェッチにはライブラリを使うべき」と React 公式がそのような意図で設計していることを公言しているのが理由です。
「ライブラリを使わずに無限ループを防ぐ」なら、依存配列の
fetchDataを省略するしかありません。(ベストではないですが、しょうがなく取る次善策として。)理解できました。ありがとうございます🙇
記事の内容については修正いたします...!
了解です!