🧗‍♀️

【React入門】絶対に理解させる(たい)useEffect【初中級者向け】

2024/08/21に公開

まえがき

🧑🏻‍💻「よし、stateの更新に応じて別のstatesetStateするコードをuseEffectで実装したぞ!!」

const [firstName, setFirstName] = useState<string>("太郎");
const [lastName, setLastName] = useState<string>("リアクト");

const [fullName, setFullName] = useState<string>("");

useEffect(() => {
   setFullName(firstName + lastName);
}, [firstName, lastName]);

👮‍♂️🚓🚨「開けろ!!!!useEffect市警だ!!!!」

今回のテーマ

今回はReactのHooksの一つである、useEffectについて解説します。useEffectは一癖も二癖もあるHookで、使い方を間違えるとパフォーマンスの低下やバグの温床になる危険性があります。
今回も初学者向けに、厳密性よりも理解させることを目的としています。

対象読者

  • React勉強中の方
  • useStateは多分いけるぞって方
  • useEffectが正しく使えているか自信のない方

useEffectって何

副作用を担当するHook...とよく言われています。「よく言われています」というのも、実はuseEffectの使用法や考え方にはいろいろな議論があり、人によって捉え方が異なります。

実際は、外部APIでデータを取得したり、React管轄外のDOM操作を行ったりする時に使われるものと捉えてくれれば大丈夫です。

副作用って何

厳密な定義はなく、曖昧ですが、Reactに限って言えば、「JSX(TSX)を生成、返却する処理、またはその為のstate管理以外の作用」と考えられます。要するにUIを表示するための処理以外の作用です。

基本的に副作用は不必要に起こるべきではなく、どうしても必要な場合にのみ適切に起こすべきであるので、useEffectはどうしても使わないといけない場合以外で使うべきではないです。

useEffectの書き方

一般化

useEffect(() => {
  // ここに処理を書く
  return () => {
    // ここにクリーンアップ関数
  };
}, [ここに依存配列]);

処理について

ここに記述される処理は、主にデータ取得、検索や直接的なDOM操作になります。
処理が走るタイミングは以下です。

  • 初回レンダリング時(マウント時)
  • 依存配列に変数を並べた際、その変数の値に変化があった場合

クリーンアップ関数について

クリーンアップ関数は、コンポーネントの純粋さを保つために必要で、APIのキャンセルやイベントリスナーの削除などを担います。
クリーンアップ関数が走るタイミングは以下です。

  • アンマウント時(レンダリング対象外になった時)
  • 依存配列の値にに変化があり再レンダリングが行われた時
    どちらのタイミングでも、前回のstate,Propsを用いてクリーンアップ関数が実行されます。

再レンダリングって何

画面を更新(再描画)するために、JavaScriptをもう一回読み込み直すことを指します。
基本的に以下のタイミングで再レンダリングが走ります。

  • 自コンポーネントのPropsが更新された時
  • 自コンポーネントのStateが変更された時
  • 親コンポーネントが再レンダリングされた時

useEffectの処理の流れ

useEffectの処理はレンダリングの後に行われます。
これによりDOMの操作が可能になっています。

(useEffectがレンダリングの最中に走ってしまうと、当たり前ですがDOM生成が終わっていないので、DOM操作を行いたくても参照するDOMそのものが無いという事態になります。なのでuseEffectはレンダリングが終わった後に走ります。)

  • 初回レンダリング時
    • useEffectの処理が発火
    • クリーンアップ関数は走らない
  • 再レンダリング時
    • 前回のstate&Propsを用いてクリーンアップ関数を実行 → 依存配列をチェックし、前回から変更があれば処理を実行
  • アンマウント時
    • 前回のstate&Propsを用いてクリーンアップ関数を実行

コードの例:データ取得

  const [results, setResults] = useState([]);
  useEffect(() => {
    let ignore = false;
    fetchAPI(query).then(response => {
      if (!ignore) {
        setResults(response);
      }
    });
    return () => {
      ignore = true;
    };
  }, [query]);

このコードは、fetchAPIという外部からデータを取得する関数を、queryが変更された際に実行するコードです。
これにより、検索ワードが変わるたびにデータを取得することができます。

では、クリーンアップ関数はどのような作用をしているでしょうか?
これは検索ワードの変更時に、前回走ったfetchAPIの結果をresultsに格納することを防ぐ為の関数です。

あなたがReactと検索したい時、タイピングを行う上で、R,Re,Rea,Reac,Reactの5つの検索ワードでfetchAPIが走ってしまいます(毎回依存配列内の変数queryが変わるため)。最後に走ったのはReactですが、この5つの検索の結果のうちどれが最後に返ってくるかは分かりません。もしかしたらReの検索結果が一番最後に返ってきてresultsに上書きされてしまうかもしれません。

それを防ぐために変数ignoreを定義します。Rを打った際に走ったfetchAPIは、Reを打った際のクリーンアップ関数ignore = trueによって、if文の中を通らなくなります(つまりsetResultsされない)。

これを繰り返すことで、素早くタイピングした場合でも、Reactの検索結果だけがresultsに格納されます(Reactを打った後に再レンダリングはされていないため、クリーンアップ関数が走らない → resultsに取得データが格納される)

コードの例:イベントリスナー

useEffect(() => {
  const keydownListener = (event) => {
    if (event.key === "Enter") {
      console.log("Enterキーが押されました");
    }
  };
  // コンポーネントがマウントされた時にリスナーを設定
  document.addEventListener("keydown", keydownListener);

  // コンポーネントがアンマウントされた時にリスナーを削除
  return () => {
    document.removeEventListener("keydown", keydownListener);
  };
}, []);

このコードでは、イベントリスナーの登録と削除を行なっています。これによって、ユーザーがエンターキーを押下した時にconsoleを出力することができるようになります。
このuseEffectの依存配列は空なので、イベントリスナーはマウント時(初回レンダー時)に登録され、アンマウント時(レンダー対象外になる時)にクリーンアップ関数が走りイベントリスナーが削除されます。

一般的にイベントリスナーの設定・削除は、ReactのDOM操作の範囲外なので、これはコンポーネントのレンダーが終わったタイミングで行われるべきです。useEffectはレンダーの後に走り、クリーンアップを行うことができるという特徴があるので、イベントリスナーの設定・削除はuseEffectを利用して実装されるべきです。

useEffectである必要がないパターン

stateの更新しかしていない場合

const [firstName, setFirstName] = useState<string>("太郎");
const [lastName, setLastName] = useState<string>("リアクト");

const [fullName, setFullName] = useState<string>("");

useEffect(() => {
   setFullName(firstName + lastName);
}, [firstName, lastName]);

これは以下のようにリファクタリングできます。

const [firstName, setFirstName] = useState<string>("太郎");
const [lastName, setLastName] = useState<string>("リアクト");

const fullName = firstName + lastName;

firstName,lastNamestateなので、変化があった時点で再レンダリングが走ります。つまり、fullNameも計算され直すので、fullNamestateで管理する必要はありません。
そもそも、stateを要素にもつstateは基本的に不要な可能性が高いです。

ちなみに、リファクタリング前のコードは内部でどのようなレンダリングが行われるか説明できるでしょうか?
firstNameを変えた際の動きを追ってみましょう。

  1. firstName"二郎"に変更🍜
  2. stateの変更を感知し、再レンダリングが走る
  3. 再レンダリングによってfirstName = "二郎",lastName = "リアクト", fullName = "太郎リアクト" でレンダリングされる(fullNameuseEffect内なので、まだ更新されていない!)
  4. レンダリング後に、useEffectが依存配列内の値の変化(firstName = "二郎")を感知
  5. setFullName(firstName + lastName);を計算する
  6. fullNameが変わったので、再レンダリングされる(stateの更新)
  7. 再レンダリングによってfirstName = "二郎",lastName = "リアクト", fullName = "二郎リアクト" でレンダリングされる
  8. 依存配列内の値は変化していないので、useEffectは発火しない

このような流れになります。不要なuseEffectのせいでレンダリング回数が増加し、パフォーマンスが低下することが理解できると思います。

まとめ

今回はuseEffectの使い方について解説しました。useEffectHooksの中でも特に使い所が難しいものとなっているので、是非正しいタイミングで使えるようになりましょう!お読みいただきありがとうございました🥳

Discussion