📝

【React修行日記】useEffectと依存配列

に公開
5

学習の目的

  • 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というルールの元リンタエラーが発生する。
これは決して無視して良いものではなく、正しく依存配列を指定する必要がある。

https://ja.react.dev/learn/removing-effect-dependencies

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を利用すべき状況は限られている。

https://ja.react.dev/learn/you-might-not-need-an-effect

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については実装経験を通してもう少し理解を深めたい

参考

https://ja.react.dev/reference/react/useEffect
https://ja.react.dev/learn/you-might-not-need-an-effect
https://ja.react.dev/learn/removing-effect-dependencies
https://zenn.dev/kiman/articles/1400b51505ac7e

Discussion

Honey32Honey32

失礼します。

「ボタンを押下するとフェッチ」「初期レンダリング時にもフェッチ」「そのためには無限ループが生じてしまうので仕方なく依存配列から fetchData を除く。そのために exhaustive-deps ルールを破る」

というのは、useEffect の使い方の説明として適切でないと思います。

そもそも、useEffect の設計意図は「発火タイミングは依存配列でコントロール」するように考えられていないからです。

依存配列がコードと一致しない場合、バグが発生するリスクが非常に高くなります。リンタを止めることで、エフェクトが依存する値について React に「嘘」をついていることになります。

代わりに、以下にある手法を使用してください。

https://ja.react.dev/learn/removing-effect-dependencies#to-change-the-dependencies-change-the-code

「useEffect の使い方」という趣旨からはズレますが、その画面を実現するためには、useEffect ではなく TanStack Query とその refetch 機能で実現するのが良いと思います。

(React 公式ドキュメントにおいても、API のデータフェッチは useEffect からではなく、フレームワークの機能や TanStack Query を使うことが推奨されています。)

https://tanstack.com/query/latest/docs/framework/react/reference/useQuery

https://ja.react.dev/learn/you-might-not-need-an-effect#fetching-data

動作確認はできていませんが、こんな感じで書けるはずです。

export default function Two() {
  const { data,  refetch, isLoading } = useQuery({
    queryKey: ["fetchRandomDogs"],
    queryFn: async () => await fetchRandomDog(),
  });
  const handleRefetch = () => {
    refetch();
  } 

  return (
    <div className="flex flex-col gap-8 items-center ">
      <div className="flex items-center justify-center w-[300px] h-[300px]">
        {isLoading && <p>🐕 画像を取得中...</p>}
        {error && <p className="text-red-500">{error}</p>}
        {data && (
          <img
            src={data.message}
            alt="ランダムな犬の画像"
            width={300}
            height={300}
            className="w-full h-full object-contain"
          />
        )}
      </div>
      <button onClick={handleRefetch}>別の画像を取得する</button>
    </div>
  );
}
tsunadogtsunadog

ご教授いただきありがとうございます...!
私の知識不足とlintの設定不備で誤った理解をしておりました🙇

以下のような理解で合っておりますでしょうか?

  • エフェクトのコードで使用されるすべてのリアクティブな値は、依存値のリスト内で宣言される必要がある。
  • APIからデータを取得する場合、依存配列にfetchDataを入れるのが本来であれば正しいが、無限ループの問題が生じる。そのため、useEffect からではなくライブラリを使用して実装するのが推奨されている。
Honey32Honey32

エフェクトのコードで使用されるすべてのリアクティブな値は、依存値のリスト内で宣言される必要がある。

これは OK です。

APIからデータを取得する場合、依存配列にfetchDataを入れるのが本来であれば正しいが、無限ループの問題が生じる。

ここまでも OK です。

しかし、「データフェッチで useEffect ではなくライブラリを使用すべき」なのは、それとは別の問題です。無限ループが起きる場合でも起きない場合でも関係なく、「データフェッチにはライブラリを使うべき」と React 公式がそのような意図で設計していることを公言しているのが理由です。

「ライブラリを使わずに無限ループを防ぐ」なら、依存配列の fetchData を省略するしかありません。(ベストではないですが、しょうがなく取る次善策として。)

tsunadogtsunadog

理解できました。ありがとうございます🙇

記事の内容については修正いたします...!