【React学習ログ】useEffectの依存配列、なぜ[]でいいのか整理してみた

この記事には広告を含む場合があります。

記事内で紹介する商品を購入することで、当サイトに売り上げの一部が還元されることがあります。

① はじめに

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]などその値が変化するたび
なし毎回のレンダリング後
[]の中に入れるのは、そのuseEffect内で使っている変数やstateやpropsです。

たとえばこんなケースが該当します。

// 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を監視することになります。

その場合の流れはこうなります。

  1. 画面表示→memosを監視→useEffect実行
  2. Firestoreからデータ取得→setMemos(data)でstateを更新
  3. memosが変わった→「依存配列の値が変化した」とReactが判断
  4. useEffectを再実行→またFirestoreからデータ取得
  5. また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は別の関数と判断するため、依存配列に入れると以下のループが発生します。

  1. レンダリング→関数生成→useEffect実行
  2. state更新→再レンダリング→新しい関数が生成される
  3. 「依存配列の値が変わった」とReactが判断→useEffect再実行
  4. ①に戻る→無限ループ

これを防ぐにはuseCallbackを使い、再レンダリングをまたいで同じ関数を使い回すようにします。

⑧ まとめ

今回、「なんとなく動いていた」[]の意味をちゃんと言語化できました。

  • []は「初回だけ実行」の指定
  • 依存配列を間違えると無限ループになる
  • 関数を入れるときはuseCallbackを検討する

次はuseCallbackとuseMemoの使いどころも整理していきたいと思います。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA



reCaptcha の認証期間が終了しました。ページを再読み込みしてください。