⚙️

React で初期化時に 1 回だけ処理を実行したいときの書き方

2023/02/01に公開

現時点で React でクラス コンポーネントを書くことは稀になったと思いますが、クラス コンポーネントあったコンポーネントのマウント時に発生する componentDidMount は関数コンポーネントではなくなりました。ではどうやるかというと useEffect を使います。

function HelloWorldComponent() {

  // これはコンポーネントの初期化時にのみ実行される
  // 依存関係に空の配列を指定する必要がある
  React.useEffect(() => {
    console.log('Hello world!');
  }, []);

  return (
    <div>Hello world!</div>
  );

}

実際には初期化時には外部の API やデータ ストアへのアクセスといったロジックが含まれます。これらはカスタム フックなどを経由して実行されることが多いです。試しに上記のコードの console.log() を何かしらのサービスの呼び出しに書き換えてみます。

function HelloWorldComponent() {

  // 何かしらのサービスを提供するカスタム フック
  const someService = useSomeService();

  // コンポーネントの初期化時にのみサービスを呼び出したい
  React.useEffect(() => {
    someService.run('Hello world!');
  }, []);

  return (
    <div>Hello world!</div>
  );

}

これは動作するのですが ESLint のルール (react-hooks/exhaustive-deps) に引っ掛かります。依存関係を指定していないからですね。なので修正します。

function HelloWorldComponent() {

  // 何かしらのサービスを提供するカスタム フック
  const someService = useSomeService();

  // コンポーネントの初期化時にのみサービスを呼び出したい
  // ESLint に怒られないように依存関係を追加する
  React.useEffect(() => {
    someService.run('Hello world!');
  }, [ someService ]);

  return (
    <div>Hello world!</div>
  );

}

おそらくこれでほとんどの場合は期待通りに動作します。ただしそれは someService の値が不変である前提においてです。こうした依存関係がある変数の値の意図しない更新によって useEffect が複数回呼ばれてしまうということはたまによくあることです。このパターンの場合はカスタム フックの作りが悪いのでそちらを直すべきではあるのですが、呼び出し側で対応が必要な場合もあります。そこで、複数回の呼び出しを禁止するためにフラグを用意します。

function HelloWorldComponent() {

  // 初期化を検知するフラグ
  const [ loading, setLoading ] = React.useState(true);
  // 何かしらのサービスを提供するカスタム フック
  const someService = useSomeService();

  // コンポーネントの初期化時にのみサービスを呼び出したい
  React.useEffect(() => {
    // すでに初期化されていたら処理を抜ける
    if (!loading) {
      return;
    }
    someService.run('Hello world!');
    // 初期化済みのフラグを立てる
    setLoading(false);
   }, [ loading, someService ]);

  return (
    <div>Hello world!</div>
  );

}

これで初期化時に 1 回のみ実行されるようにすることができます。

正しく動いているように見えるけど開発者ツールで除くと同じ API を 2 回呼び出していることがある、というのは本当によくあるあるなので、意識して実装していきたいものですね。

Discussion