useEffectの特性と注意点

React_icon React
この記事は約16分で読めます。

この記事は、以下の方を対象に書いています。

  • useEffectについての知識を深めたい
  • 注意点を知りたい

はじめに

この記事は、私自身がuseEffectの挙動を理解しないまま使用していたために、

発生していた問題の備忘録として書いています。

useEffectは便利な反面、使い方を間違えると思わぬバグが発生してしまいます。

今回は、useEffectの特性と注意点について解説します。

useEffectとは?

useEffectとは、Reactのhooks(フックス)の一つです。

利用頻度はかなり高いので、基礎から押さえておくと、思わぬ事故を防ぐことができます。

まず、基本的な使い方は以下になります。

useEffect(() => {
  // any code
}, []);

Reactを勉強し始めた方にとっては、かなり複雑に見えますが、これは慣れるしかありません。

まず、useEffectには引数が2つあります。

一つ目は、「() => {}」の部分。

ここでは、useEffectが呼ばれたときに、実行する処理を書きます。

二つ目は、「[]」の部分。

この[]は依存配列と呼ばれます。

ここでは、useEffectがどのタイミングで呼ばれるかを書きます。

サンプルコードでは、「[]」だけなので、今のページが最初に描画されたタイミングで呼ばれます。

他には、「[state]」であれば、stateという変数の値が書き変わった時に呼ばれます

useEffectの特性

次は、useEffectの特性について解説していきます。

非同期性

useEffectの中で、非同期処理の実行をすることが可能です。

これにより、値変更時のデータ取得やAPI側へのトリガーにすることも出来るようになり、

汎用性が上がります。

非同期処理については、別の記事にて解説していますので、

気になる方はそちらを確認してみてください。

ただ、useEffect内で非同期処理をする場合は、少し特殊な書き方をします。

useEffect自体を非同期関数にすることはできないので、

useEffect内で非同期関数を作成し呼び出すか、

別で作成した非同期処理をuseEffect内で呼び出す必要があります。

以下は、前者のサンプルコードです。

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('https://sample/url');
        if (!response.ok) {
          throw new Error('エラー発生');
        }
        const responseData = await response.json();
        console.log(responseData);
      } catch (error) {
        console.error('Error data:', error);
      }
    };
    fetchData();
  }, []);

実行タイミングの制御

useEffectは、実行のタイミングの調整が可能です。

useEffectの第2引数(依存配列と呼ぶ)に変数を設定することで、その変数の監視をします。

監視中に、値の変化があった際にuseEffect内の処理が実行されます。

※変更を検知したら、検知した回数分の処理が実行されます。

また、後述しますが、厳密には値の参照が変化したタイミングで実行されます。

また、依存配列に空の配列を指定した場合は、初回レンダリング(画面描画)時に実行されます。

useEffectの注意点

useEffectを使用する場合、注意しなければならないことがいくつかあります。

ここではその注意点について、説明します。

無限ループの発生

useEffect内でStateの更新をする場合、

その更新がuseEffectの実行条件を満たしてしまう可能性があります。

無限ループが発生しないように依存配列は正しく設定しましょう。

以下は、無限ループが発生する例です。

import './App.css';
import { useEffect, useState } from 'react';

function App() {
  const [count, setCount] = useState<number>(0);

  useEffect(() => {
    setCount(prevCount => prevCount + 1);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
    </div>
  );
}

export default App;
実行結果(無限ループ)

上記のサンプルコードでは、依存配列にcountという変数を設定していますが、

useEffect内でcountの更新をしているために無限ループが発生してしまいます。

countの更新をする際は、ボタンを押したときなどのトリガーを設定するなどの、

対応は必要になります。

以下の修正例は、useEffectは使用せずにボタンでcountを更新する関数を設定した例です。

useEffectは便利ですが、使用する場面を見極めることが大切です。

import './App.css';
import { useEffect, useState } from 'react';

function App() {
  const [count, setCount] = useState<number>(0);

  // useEffect(() => {
  //   setCount(prevCount => prevCount + 1);
  // }, [count]);

  const countUp = () => {
    setCount(prev => prev + 1);
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={countUp}>{'CLICK'}</button>
    </div>
  );
}

export default App;

ただ、countを常に更新したい場合は、後述するsetInterval関数の使用をするといいと思います。

依存配列の使い方

useEffectの便利なとこは、依存配列を設定して実行条件を絞れるとこです。

ただし、依存配列に不適切な値を設定してしまうと思わぬバグの発生につながる可能性もあります。

特にコード量が多くなるプログラムの場合は、注意が必要です。

まず、依存配列は空の配列の場合は初回のみ実行されます。

依存配列が指定されている場合は、その配列内の変数が更新された場合に、

useEffect内の処理が実行されます。

また、依存配列には、カンマ区切りで複数の変数を設定することも可能です。

string型やnumber型の変数の監視をする場合は、比較的シンプルなので、

思った通りの挙動を再現しやすいですが、オブジェクトの監視をする場合は少し注意が必要です。

依存配列に変数を設定した場合に、値が更新される条件は、その変数の参照が更新された場合です。

例えば、以下のサンプルコードでは、ボタンを押してもstateは更新されず、

useEffect内の処理は実行されません。

import './App.css';
import { useEffect, useState } from 'react';

interface Item {
  name: string
  qty: number
}

function App() {
  const [item, setItem] = useState<Item>({ name: 'いちご', qty: 0 });
  const [message, setMessage] = useState<string>('');

  useEffect(() => {
    console.log('ok');
    setMessage(`${item.name}が${item.qty}に更新されました。`);
  }, [item]);

  const countUp = () => {
    const it = item;
    it.qty = it.qty + 1;
    setItem(it);
  }

  return (
    <div>
      <p>{message}</p>
      <p>{`name:${item?.name}`}</p>
      <p>{`qty:${item?.qty}`}</p>
      <button onClick={countUp}>{'CLICK'}</button>
    </div>
  );
}

export default App;

この原因は、依存配列に設定した変数の参照が変わらないために、

useEffectの実行条件を満たしていないからです。

修正したコードでは、19行目で、スプレッド構文(…変数)を使用して、

新しいオブジェクトを作成した上で、qtyを更新しています。

そのため、useEffectの実行条件を満たします。

import './App.css';
import { useEffect, useState } from 'react';

interface Item {
  name: string
  qty: number
}

function App() {
  const [item, setItem] = useState<Item>({ name: 'いちご', qty: 0 });
  const [message, setMessage] = useState<string>('');

  useEffect(() => {
    console.log('ok');
    setMessage(`${item.name}が${item.qty}に更新されました。`);
  }, [item]);

  const countUp = () => {
    setItem({ ...item, qty: item.qty + 1 });
  };

  return (
    <div>
      <p>{message}</p>
      <p>{`name:${item?.name}`}</p>
      <p>{`qty:${item?.qty}`}</p>
      <button onClick={countUp}>{'CLICK'}</button>
    </div>
  );
}

export default App;

クリーンアップをする

クリーンアップとは、setIntervalなどの定期的に繰り返される関数を使う場合に、

リソースの開放をすることです。簡単に言うと、「処理の中止」のことです。

例えば、以下のコードだと、半永久的に処理が繰り返されます。

import './App.css';
import { useEffect, useState } from 'react';

function App() {
  const [count, setCount] = useState<number>(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(count + 1);
    }, 1000);
  }, []);

  return (
    <div>
      <p>{`count: ${count}`}</p>
    </div>
  );
}

export default App;

この半永久的に繰り返されるコードは、小規模なプロジェクトであれば、

特に問題にはならないと思いますが、規模が大きくなるにつれて、

処理内容が重くなって、全体の動作に影響する場合があります。

ですので、適切なタイミングでリソースの開放をしましょう。

以下は、修正例です。

clearInterval関数を使用して、リソースの開放をすることで、

初回だけ呼ばれるようになります。

import './App.css';
import { useEffect, useState } from 'react';

function App() {
  const [count, setCount] = useState<number>(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(count + 1);
    }, 1000);

    return () => {
      clearInterval(interval);
    }
  }, []);

  return (
    <div>
      <p>{`count: ${count}`}</p>
    </div>
  );
}

export default App;

useEffectの使い道

useEffectは、使えるようになると便利で手放せなくなりますが、

React初学者にとっては、書き方が複雑だったり、どのような場面で使うかが、

イメージしづらいと思いますので、いくつかサンプルコードを紹介します。

サンプルコード①

まずは、テキストフィールドに文字を入力してから2秒経過すると、

メッセージを表示するプログラムを作成してみます。

import './App.css';
import { useEffect, useState } from 'react';

function App() {
  const [text, setText] = useState<string>('');
  const [message, setMessage] = useState<string>('');

  useEffect(() => {
    if (text.length === 0) {
      return;
    }
    const timeoutId = setTimeout(() => {
      setMessage('入力完了後、2秒経過しました。');
      console.log('OK');
    }, 2000);

    return () => {
      setMessage('');
      clearInterval(timeoutId);
    }
  }, [text]);

  return (
    <div>
      <input type="text" value={text} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setText(e.target.value)}></input>
      <p>{message}</p>
    </div>
  );
}

export default App;

今回は、setTimeout関数を使用してみました。

setTimeout関数は、第1引数に実行したい処理、

第2引数に処理開始タイミングをミリ秒単位で指定します。(上記では2000ミリ秒なので2秒)

また、最後の文字を入力してから2秒間のカウントをしたかったので、

useEffectの最後にクリーンアップをして、setTimeout関数の初期化をしています。

サンプルコード②

次は、少し簡単になりますが、2つのテキストフィールドに入力した数値の

合計値をリアルタイムに表示するプログラムを作成してみます。

import './App.css';
import { useEffect, useState } from 'react';

function App() {
  const [num1, setNum1] = useState<number>(0);
  const [num2, setNum2] = useState<number>(0);
  const [message, setMessage] = useState<string>('');

  useEffect(() => {
    setMessage(`合計値:${num1 + num2}`);
  }, [num1, num2]);

  return (
    <div>
      <input type="number" value={num1} onChange={(e) => setNum1(Number(e.target.value))} />
      <input type="number" value={num2} onChange={(e) => setNum2(Number(e.target.value))} />
      <p>{message}</p>
    </div>
  );
}

export default App;

ポイントは、依存配列に変数を2つ入れて、num1とnum2の監視をすることで、

どちらかの変更を検知し合計値の計算をしている箇所です。

少し、応用すると、割と簡単に電卓プログラムも作れますね!

他にも実用的な応用例でいうと、合計値が10以上になった場合に非同期通信を行い、

APIからデータを取得するなどのことも可能になります。

まとめ

今回は、Reactのフックスの1つであるuseEffectについて解説しました。

Reactのプロジェクトでは、ほぼ確実に使用すると思いますので、

たくさん使ってuseEffectを使ったサンプルコードをたくさん書いて練習してみてください。

2か月くらいしたらuseEffectの虜になっているはずです(笑)

それでは、ここまで読んでいただきありがとうございました。

また次の記事でお会いしましょう!!

コメント

タイトルとURLをコピーしました