この記事には広告を含む場合があります。
記事内で紹介する商品を購入することで、当サイトに売り上げの一部が還元されることがあります。
① はじめに
Firebaseを使ったメモアプリを実装中、useEffectの依存配列に[]を書きました。
動作はしていましたが、なぜ空でいいのかは「なんとなくの理解」でした。
せっかくなので、改めて挙動を整理してみました。
② そもそもuseEffectとは
useEffectは、Reactコンポーネントの「副作用」を扱うhookです。
副作用とは、レンダリング以外の処理のことです。
たとえばAPIやFirestoreからのデータ取得、タイマーの設定などが該当します。
基本的な書き方はこうなります。
useEffect(() => {
// 副作用の処理
}, [依存配列]);③ レンダリングとは何か
レンダリングとは、コンポーネントが画面に表示するJSXを計算・描画する処理のことです。
具体的にはこの部分です。
return (
<div>
<h1>{title}</h1>
<p>{content}</p>
</div>
);Reactはこのreturnの中のJSXをもとに「画面に何を表示するか」を決めます。
たとえばこのコードがあったとします。
const title = "メモ一覧";
const count = 3;
return (
<div>
<h1>{title}</h1>
<p>メモが{count}件あります</p>
</div>
);Reactはこれを見て、{title}を「メモ一覧」に、{count}を「3」に置き換え、画面に表示するHTML構造を組み立てます。
stateやpropsが変わるたびにこの計算がやり直され、画面が更新されます。
これが再レンダリングです。
Reactの設計思想として「レンダリングは純粋な計算だけにする」という考え方があります。
データ取得などの「外部への作用」はレンダリングとは分離してuseEffectに書く、というのがその理由です。
| 種類 | 具体例 |
|---|---|
| レンダリング(画面描画) | JSXの計算・DOM更新 |
| 副作用(それ以外) | データ取得・タイマー・ログ出力など |
④ 依存配列の役割
依存配列は、「このuseEffectをいつ実行するか」を指定するものです。
| 書き方 | 実行タイミング |
|---|---|
| [] | 初回レンダリング時のみ |
| [userId]など | その値が変化するたび |
| なし | 毎回のレンダリング後 |
たとえばこんなケースが該当します。
// userIdが変わるたびにそのユーザーのメモを取得したい
useEffect(() => {
getDocs(query(collection(db, "memos"), where("uid", "==", userId)));
}, [userId]);stateの値は変化を監視したい場合に入れます。
const [filter, setFilter] = useState("all");
useEffect(() => {
// filterが変わるたびに絞り込み結果を更新
}, [filter]);⑤ stateを依存配列に入れるのはどんな時か
「useEffectの中でその値を使っていて、かつ値が変わるたびに処理を再実行したい時」です。
検索・フィルター
const [filter, setFilter] = useState("all");
useEffect(() => {
getDocs(query(collection(db, "memos"), where("category", "==", filter)));
}, [filter]);ユーザーIDに応じたデータ取得
const [userId, setUserId] = useState(null);
useEffect(() => {
if (!userId) return;
getDocs(query(collection(db, "memos"), where("uid", "==", userId)));
}, [userId]);入力値に連動した処理
const [searchWord, setSearchWord] = useState("");
useEffect(() => {
searchMemos(searchWord);
}, [searchWord]);判断基準をひとことで言うと、useEffectの中でそのstateを使っていて、変化に反応して処理を動かしたい場合に入れます。
逆に「一度だけ実行すればいい」場合は[]にするのが正解です。
⑥ メモアプリでの実際のコード
実装したメモアプリでは、こんな形でデータを取得していました。
useEffect(() => {
const q = query(collection(db, "memos"));
getDocs(q).then((snapshot) => {
const data = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setMemos(data);
});
}, []);依存配列を[]にしている理由は、画面表示時に一度だけデータを取得すれば十分だからです。
ここにmemosを入れてしまうと、memosを監視することになります。
その場合の流れはこうなります。
- 画面表示→memosを監視→useEffect実行
- Firestoreからデータ取得→setMemos(data)でstateを更新
- memosが変わった→「依存配列の値が変化した」とReactが判断
- useEffectを再実行→またFirestoreからデータ取得
- またmemosが変わる→③に戻る→無限ループ
つまり「memosを監視する」=「memosが変わるたびにFirestoreを取得する」なので、取得するたびにmemosが変わり、永遠に止まらなくなります。
これはJavaScriptの仕様も関係しています。
オブジェクトや配列は中身が同じでも別物として扱われます。
const a = [{ text: "メモ1" }];
const b = [{ text: "メモ1" }];
console.log(a === b); // false ← 中身が同じでも別物!そのためFirestoreから取得するたびに、前回と中身が同じデータでもReactは「別の値が来た=変化した」と判断してしまいます。
⑦ ハマりポイントまとめ
整理すると、依存配列で気をつけることは主に2つです。
1. 「毎回実行したくない処理」には必ず[]を入れる
何も書かないと毎回実行されてしまいます。
2. 関数を依存配列に入れるときは注意
コンポーネント内で定義した関数は、再レンダリングのたびに新しく作り直されます。
中身が同じでもReactは別の関数と判断するため、依存配列に入れると以下のループが発生します。
- レンダリング→関数生成→useEffect実行
- state更新→再レンダリング→新しい関数が生成される
- 「依存配列の値が変わった」とReactが判断→useEffect再実行
- ①に戻る→無限ループ
これを防ぐにはuseCallbackを使い、再レンダリングをまたいで同じ関数を使い回すようにします。
⑧ まとめ
今回、「なんとなく動いていた」[]の意味をちゃんと言語化できました。
- []は「初回だけ実行」の指定
- 依存配列を間違えると無限ループになる
- 関数を入れるときはuseCallbackを検討する
次はuseCallbackとuseMemoの使いどころも整理していきたいと思います。