|

useEffectの復習(復讐?)

useEffectは恐ろしい。
なんとなく分かったつもりになってたはずなのに、すぐ分からない状態になってしまう。
もう一度YouTubeとかを見て、そうか、なるほどと思った自分は何も分かっていなかった。
サンプルコードの意味はわかるけど、自分のコードに必要なのかどうかまでは分からない。
そのサイクルをもう4周くらい繰り返しているが、今回は果たして・・・?

useEffectについて覚えていること

自分がuseEffectの何が分かってて何が分かっていないのか、まずはuseEffectについて覚えていることを書き出してみる。

  • React Hooksの一種である
  • APIのデータを取ってくるときに使われるイメージ
  • コードはこんな感じで、最後のカンマの後に[] がempty array なら最初だけ実行されて、何か入れた場合はその値が変わるたびに実行される
JSX
useEffect(() => {
//ここに実行したいコードを書く
  }, []);

useEffect、やっぱりよく分かってない。
もう一度、公式サイトを見てみる。

useEffect は、コンポーネントを外部システムと同期させるための React フックです。

https://ja.react.dev/reference/react/useEffect#useeffect

・・・やばい、一行目からもう何言っているか分からない。
なんのために使うか分かってないのがまず問題だと認識できたところで、詳しく見ていく。


「外部システム」の意味

読み進めると”外部システムに接続していない場合は、おそらくuseEffectを使う必要がない”とも書いてあるので、まずは外部システムが何のことか分からないと、useEffectを使うかどうか判別できないことになる。

外部システムへの接続例

(1)分かりやすいのはAPIでデータを取得したり、Firestore等のデータベースへデータを追加したり呼び出したりするというとき。確かにそういう場面でuseEffectを使っているイメージだし自分のPC内のデータではなく、インターネット上からデータを取っているので外部にあるデータと同じものを表示させているので同期というのもわかる。

(2)マウスの動きに合わせて、とかwindowの幅に合わせて何かを実行したいときのEventListenerも外部システム。私の違和感はこれかも。”外部感”が薄いというか、思っていた外部と内部の線引きじゃなかったというか。でもレンダリング(Rendering)とかDOMが何かを再確認したら、少し見えるようになってきた。

つまり、Reactにとって、ブラウザのDOMは外部ということ。”root”の中はReactで変えているけど、rootより外側のwindow部分につけるEventListenerはReactの範疇外。rootの内側でも、document.getElementByIdのような形であればdocumentはwindowオブジェクトのプロパティなのでブラウザのDOMを操作することになり、外部ということになる。Reactで急にdocumentを使わなくなったのはそういうことか。

「同期」と「非同期」

「同期」というのは相互の情報の共有をして同じ状態に保つことの意味。写真とか”X項目をiCloudに同期中”と出てくるし、ここでいう「外部システムと同期」というのもその意味と理解。これは問題ない。
ただ、useEffectの使い方を調べていると「非同期」という言葉がよく出てくる。この言葉が最初わかりづらかった。

非同期的な処理で「外部システムと同期」する、ということか。

副作用

useEffectの”Effect”は”side effect(副作用)”という言葉から来ているというくらい、副作用のためのHookらしいが、Reactでの本作用(?)は何なのか。それはコンポーネントのビュー(表示)をレンダリングすること。つまりここで言う副作用とは、本作用で表示を更新した際に、もしもこの部分にも変更が発生していたら、useEffectの中のコードも実行してね。実行した後はもう一回レンダーして表示をリフレッシュさせてね、というだけのことなのだが、個人的に「副作用」という言葉がしっくり来なかった。何となく薬の副作用という言葉のイメージが強く、求める効果を得る代わりに犯す犠牲とかリスクみたいなニュアンスをどこかに感じてしまっていた。useEffectでやりたいことは本当にやりたいことだし、変わってほしくないのに勝手に変わってしまうとかそういうことでもないのになぜ副作用というのかなと。
プログラムの文脈での「副作用」は、関数やメソッドの実行によって、その関数やメソッド自体でreturnする値以外の変更や効果をもたらすことを指しているということが分かった。

(比較)useEffectを使わなくていいケースと使うケース

用語の意味を再確認できたところで、今の話をコードで見てみる。
例えば「ボタンをクリックすると次のページが見られる」という場合、useEffectを使わなくていいケースと使うケースをそれぞれイメージする。

ボタンクリックで単に別のコンポーネントを呼び出しているだけで、外部システムとの同期はしていないのであれば、表示がレンダーされて、変化も起こるが、useEffectは使われない。

JSX
import React, { useState } from 'react';

const MyComponent = () => {
  const [showNextPage, setShowNextPage] = useState(false);

  const handleButtonClick = () => {
    setShowNextPage(true);
  };

  if (showNextPage) {
    return <NextPageComponent />;
  }

  return (
    <div>
      <button onClick={handleButtonClick}>次のページへ</button>
    </div>
  );
};

一方で、以下のようにボタンをクリックするとAPIから情報を取得し、お天気情報<WeatherInfoComponent>を表示したいという場合には「外部システムとの同期」が必要なため、useEffectが使われる。

JSX
import React, { useState, useEffect } from "react";

const MyComponent = () => {
  const [cityWeatherData, setCityWeatherData] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [city, setCity] = useState(null);

  //いずれかのCityボタンがクリックされると、cityの値が更新されてuseEffectがトリガーされる
  useEffect(() => {
    if (city) {
      setIsLoading(true);
      // 外部APIとの同期
      fetch(`https://api.example.com/?city=${city}`)
        .then((response) => response.json())
        .then((data) => {
          setCityWeatherData(data);
        })
        .catch((error) => {
          console.error("データの取得に失敗しました:", error);
        })
        .finally(() => {
          setIsLoading(false);
        });
    }
  }, [city]);
  
  //通信中はisLoadingの値がtrueになり、Loading画面が表示される
  if (isLoading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <div>
        <button onClick={() => setCity("tokyo")}>Tokyo</button>
        <button onClick={() => setCity("melbourne")}>Melbourne</button>
        <button onClick={() => setCity("sandiego")}>San Diego</button>
      </div>
      {/* cityWeatherDataが取得でき次第、WeatherInfoComponentが表示される */}
      {cityWeatherData && <WeatherInfoComponent data={cityWeatherData} />}
    </div>
  );
};

クリーンアップ関数

最後に、クリーンアップ関数について見ていく。
Reactは仮想DOMの更新の前後でuseEffectの依存配列(カンマの後の[]部分)にある値に変更があったかどうかを監視して、変更があった場合にはuseEffectの中のコードを実行しているが、useEffectの中でイベントリスナーを追加したり、setIntervalなどのタイマーを設定したりする場合には、クリーンアップ関数を追加しないといけない。(イベントリスナーやタイマーを設定していないのであれば、クリーンアップ関数は不要。)

OK、分かった。と言いたいところだが、追加しないとどうなるのかよく分からないので見てみる。

クリーンアップ関数を入れ忘れるとどうなるか

まず手元でTop Page用のComponentを用意して、Page AとPage Bのリンクを貼り、Page Aをクリックすると以下のコードが実行され、Page Bをクリックすると別のComponentが呼ばれるように設定した。<PageAComponent>では、useEffect使用して1秒ごとにカウントアップし、その値をcountに設定して表示している。

8行目に目印となるconsole.logを入れて、
13行目のclean up functionをコメントアウトしておく。
(2回ずつ実行されないように<React.StrictMode>は消してある。)

JSX
import React, { useState, useEffect } from 'react';

const PageAComponent = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalID = setInterval(() => {
      console.log("PageAComponentから離れても実行され続ける") // 目印
      setCount(prevCount => prevCount + 1);
    }, 1000);
    
    // MyComponentから離れる時にsetIntervalメソッドを止めるためのclean up function
    //return () => {
    //  clearInterval(intervalID);
    //};
        
  }, []);

  return <div>Count: {count}</div>;
};

setInterval() メソッドで 、一定の遅延間隔を置いてsetCountを更新しているが、この部分は<PageAComponent>から離れて別のコンポーネントを呼び出して、画面上にcountが現れなくなっても、裏側でsetInterval()メソッドが動き続けていて、コンピュータに無駄に負荷をかけることになる。

こうならないように、<PageAComponent>から離れるときに呼びたいコード(ここではclearInterval(intervalID))を書いておくということ。

clean upの必要性も分かったところで本日はここまで!


動画編集のためにCanvaを使ってみたのだが、
そのついでにText to Imageを生成するAIというのも試してみた。
何これ・・・?このAIはカモノハシを全く知らないようです。

canvaAI

類似投稿