技術っぽいことを書いてみるブログ

PythonとかVue.jsとか技術的なことについて書いていきます。

Reactについて学ぶ~コンポーネントの副作用について考える(useEffect)編~

はじめに

今回は、ReactのuseEffectについてまとめておきます。

useEffectを考えるうえで、「副作用」というワードが出てきますが、
なかなかピンとこないので、言葉の定義は置いておいて、早速本編に行きます。

useEffectの使い方①

useEffectの使いどころを考えるにあたり、NGパターンを示します。

  • NGパターン
import { FC, useEffect, useState } from 'react';

const AboutEffect: FC = () => {
  const [prise, setPrice] = useState(100);
  const [discountPrice, setDiscountPrise] = useState(0);

  const onChangePriceHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPrice((p) => Number(e.target.value));
    discount();
  };

  const discount = () => {
    setDiscountPrise(prise - 10);
  };

  return (
    <div>
      <div>
        <label>金額(値引き前)</label>
        <input type="text" value={prise} onChange={onChangePriceHandler} />
      </div>
      <div>
        <label>金額(値引き後)</label>
        <span>{discountPrice}</span>
      </div>
    </div>
  );
};

export default AboutEffect;

このプログラムは、値引き前金額を入力すると、入力された金額から10円を引いた金額が金額(値引き後)に表示されるものですが、
金額(値引き前)に「10」と入力されると、金額(値引き後)は、「-9」となります。

これは、「10」と入力したあと、priceの値は変更し終わっていないタイミングでdiscount関数が実行され、
discount関数でpriceを参照しても、前の値が取得されてしまうためです。

このような事象を防ぐ方法が、useEffectで、
state(状態)の変更に応じて、実行する関数を指定することができます。

  • useEffectを使った実装
import { FC, useEffect, useState } from 'react';

const AboutEffect: FC = () => {
  const [prise, setPrice] = useState(100);
  const [discountPrice, setDiscountPrise] = useState(0);

  const onChangePriceHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPrice((p) => Number(e.target.value));
  };

  useEffect(() => {
    setDiscountPrise(prise - 10);
  }, [prise]);

  return (
    <div>
      <div>
        <label>金額(値引き前)</label>
        <input type="text" value={prise} onChange={onChangePriceHandler} />
      </div>
      <div>
        <label>金額(値引き後)</label>
        <span>{discountPrice}</span>
      </div>
    </div>
  );
};

export default AboutEffect;
  • 解説
    • useEffectの第一引数は、引数を持たない関数を受け取ります。
    • useEffectの第二引数は、変数の配列が渡せます。(依存配列)
      • この配列の中に格納された変数がひとつでも前のレンダリング時と比較して差分があったときだけ、第 1 引数の関数が実行されます。
      • 上記の例だと、priseの値が変更されたので、第一引数の関数が動いて、値引き金額が導出されています。
    • useEffectの第二引数を[]のように空配列を渡すと、初回のレンダリング時にのみ第 1 引数の関数が実行されます。
      • APIからデータを取得する場合とかに使用すると良いでしょう

2023/10/30 追記

  • id:honey321998 さんからコメントを頂きました。
    • この例で挙げた実装は、「derived state (派生した状態)」と呼ばれているものに該当し、useEffect を使わずに、生の式を書くだけで実装できるとのことです。
    • 参考ページ
    • ということで、修正版となります。
import { FC, useEffect, useState } from 'react';

const AboutEffect: FC = () => {
  const [prise, setPrice] = useState(100);

  const onChangePriceHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPrice((p) => Number(e.target.value));
  };

  const discountPrice = prise - 10;

  return (
    <div>
      <div>
        <label>金額(値引き前)</label>
        <input type="text" value={prise} onChange={onChangePriceHandler} />
      </div>
      <div>
        <label>金額(値引き後)</label>
        <span>{discountPrice}</span>
      </div>
    </div>
  );
};
export default AboutEffect;

useEffectの使い方②

  • サンプルプログラム
    • 実行すると30から1秒ごとにカウントダウンするカウンターが表示されます。
    • さらに、「+1」ボタンを押下すると押下した回数が表示される機能があります。
import type { FC } from 'react';
import { useEffect, useState } from 'react';

const MAX_TIME = 30;

const Timer: FC = () => {
  const [timeLeft, setTimeLeft] = useState(MAX_TIME);
  const tick = (): void => setTimeLeft((t) => t - 1);
  const reset = (): void => setTimeLeft(MAX_TIME);
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timerId = setInterval(tick, 1000);
    return () => clearInterval(timerId);
  }, []);
  useEffect(() => {
    console.log('useEffect');
    if (timeLeft === 0) setTimeLeft(MAX_TIME);
  });

  const countUp = () => {
    console.log('countup');
    setCount((c) => c + 1);
  };
  return (
    <>
      <div style={{ marginBottom: '20px' }}>
        <div>
          <label>Count</label>
          <span>{timeLeft}</span>
        </div>
        <button onClick={reset}>Reset</button>
      </div>
      <div>
        <span>{count}</span>
        <button onClick={countUp}>+1</button>
      </div>
    </>
  );
};
export default Timer;
  • このプログラムを実行し、「+1」ボタンを押下すると…
    • countupのログ出力と、useEffectのログ出力が、同時に出力されます。
    • つまり、「+1」ボタンを押下したタイミングで、countUp関数だけでなく、2つ目のuseEffectの関数も実行されています。
    • 何が問題か…
      • useEffectの第二引数に何も設定していないことで、「+1」ボタンを押下したことによるレンダリングをトリガに2つ目のuseEffectの関数が動いているのです。
  • どうするか?

    • 二つ目のuseEffectの第二引数に検知対象の状態を指定するようにします。
  • さらに解説

    • useEffectの第一引数は、依存配列に変更があった際に実行される関数になりますが、戻り値としてクリーンアップ関数を返すことができます。
    • クリーンアップ関数では、コンポーネントがアンマウントされるタイミングで実行したい関数となります。
      • 上記の例では、タイマーのクリア処理の関数を返しています。
      • 他にも、socket通信のdisconnect処理とかにも使用できそうです。

最後に・・・

なかなか言葉にするのは難しいですね。

まぁ、自分の備忘録になればよいか…。

次回は・・・、Reactの何かです。ルーターとかかな…。