外部システムと同期を行うuseEffectは、React hookのなかでも特に取り扱いの難しいフックです。

useEffectを使えるようになれば、以下のようなアプリも作れるようになります。


useEffectで作ったタスク管理アプリ


本記事で、useEffectの基本的な使い方をマスターしておきましょう。


1.Reactコンポーネント内にある3種類のロジック

useEffectについて理解するためには、Reactのロジックについて知っておく必要があります。

Reactのロジックは、大きく分けると、以下の3種類あります。



useEffectは、3番目のロジック(エフェクト:レンダリングが起点となって動作するもの)に関係するフックです。

そのため、上記の3つのロジックについてよく知っておくことで、useEffectについてより深く理解し、適切に扱えるようになります。


1-1.レンダーコード

レンダーコードは、Reactコンポーネントのベースになるものです。

「入力(propsやstate)を受け取り、UI(JSX)に変換」という処理を、レンダーコードは行います。


レンダーコードの例

functionComponent({ a }){
  return<div>{a}</div>;
}


レンダーコードは数学でいう方程式のように、「同じ入力なら必ず同じ出力になる(純粋関数)」ように設計されている必要があります。


1-2.イベントハンドラ

イベントハンドラは、ユーザーの「テキストフィールドに文字を入力する」「ボタンをクリックする」といったアクションをトリガーに、処理を行うロジックです。


イベントハンドラの例

functionhandleClick(){
  fetch();
}
<button onClick={handleClick}>購入</button>


1-3.エフェクト

エフェクトは、レンダーをトリガーに処理を実行するロジックです。


エフェクト(useEffect)の例

useEffect(()=>{
  dataFtch();
},[]);


エフェクトは、ユーザーの操作を必要とするイベントハンドラと異なり、画面の更新(表示)に合わせて動作します。


2.useEffectとは

useEffectは、上記の「1-3.エフェクト」でも紹介したとおり、レンダーをトリガーに処理を実行するフックで、React外のシステムと連携する際に使用します。


useEffectの例

useEffect(()=>{},[]);


なお、useEffectは「避難ハッチ」として用意されたフックです。

基本は使用を避け、どうしてもReact内で完結できないシステムを構築(つまりReact外のシステムとの連携)しなければならないときのみ使用します。


React外のシステムの例


3.useEffectの基本構文

useEffectの構文は以下の通りです。


useEffectの構文例

useEffect(setup, dependencies) 

useEffectの各要素の意味

useEffect(セットアップ関数, 依存配列) 


3-1.引数:依存配列

以下の例文の「dependencies」に当たる要素が、useEffectを実行させるトリガーとなる「依存配列」です。

useEffect(setup, dependencies) 


依存配列に記載する要素は、後述する「セットアップ関数」で参照しているすべてのリアクティブな値です。


依存配列に記載する「リアクティブな値」の例


Reactは依存配列が変更されているかどうか確認し、どれかが変われば、後述するクリーンアップ関数とセットアップ関数の実行を行います。


なお、依存配列の書き方は以下のように3通りあります。



useEffectに依存配列を渡す3種類のパターン


依存配列を記述する際の注意点

依存配列は「要素数が一定」「インライン」で記述する必要があります。

そのため、以下のように宣言した配列を依存配列に渡すということはできません。

const deps =[a, b, c];
useEffect(()=>{
  //セットアップ関数
},[deps]);


3-2.引数:セットアップ関数

以下の例文の「setup」に当たる要素が、useEffectで実行するロジックを記述した「セットアップ関数」です。

useEffect(setup, dependencies) 


セットアップ関数は、コンポーネントがコミットされると実行されます。


なお、セットアップ関数には、クリーンアップ関数を設定することもできます。

useEffect(()=>{
  /*
    セットアップ関数の処理内容
  */
  return()=>{
    /*
      クリーンアップ関数の処理内容
    */
  };
},[dep1, dep2, etc…]);


クリーンアップ関数は、「依存配列(dependencies)の値が変更されてレンダーが起きる」と、新たにセットアップ関数が実行される前に、実行されます。

また、アンマウント(コンポーネントが画面(DOM)から削除)される場合にも実行されます。


「レンダー」と「コミット」について

Reactは、以下のステップ(1〜3)を経て画面を表示します。


  1. トリガー:「コンポーネントが初めて読み込まれる」「自身(あるいは祖先の)コンポーネントの状態(state)が変化する」などで更新が起きる
  2. レンダー:Reactがコンポーネントを呼び出し、次に描画する画面(DOM)の内容を計算する
  3. コミット:計算結果を画面(DOM) に反映する


上記で述べたセットアップ関数は、描画(1〜3ステップ)が完了したあとに、実行されます。

参考:レンダーとコミット – React


3-3.返り値

useEffectはundefinedを返します。

つまり、useEffect自体は値を返しません。

よって、以下のような使い方で、値を受け取ることはできないため注意が必要です。

const x =useEffect(()=>{ セットアップ関数 });


4.useEffect内で非同期処理を実行する方法

「3-3.返り値」でも触れたように、useEffect内で非同期処理を実行したい場合、以下のように、セットアップ関数に直接非同期処理を書くと、エラーが発生します。

useEffect(async()=>{
    const res =awaitfetch(param);
    const json =await res.json();
    setData(json);
  },[]);


useEffect内で非同期処理を正しく実行するには、以下のような方法があります。



それぞれ、詳しく見ていきましょう。


4-1.useEffect内でasync関数を定義して呼び出す方法

useEffect内での非同期処理としては、この方法がもっとも一般的な方法です。

useEffect(()=>{
  asyncfunctiongetData(){//非同期処理用の関数を定義
      const res =awaitfetch(param);//リクエストを送信
      const json =await res.json();
      setData(json);//取得したデータをstateに渡す
    }
  }
  getData();//定義したasync関数を呼び出す
},[param]);


なお、useEffect内で実行する非同期処理の大半は、データ取得です。

取得するデータが大きく、非同期処理に時間がかかるケースでは、途中で新しいfetchが呼び出される場合があります。

その場合は、AbortControllerを使いましょう。


AbortControllerを使った例

useEffect(()=>{
  const ac =newAbortController();//処理を中断するためのインスタンス生成
  asyncfunctiongetData(){
    try{
      const res =awaitfetch(param,{signal: ac.signal });//signalプロパティ付きのリクエストを送信
      const json =await res.json();
      setData(json);
    }catch(e){
      if(e.name !=="AbortError"){//処理中断時以外のエラーを出力
        console.error(e);
      }
    }
  }
  getData();
  return()=>{//クリーンアップ
    ac.abort();//実行中の古いfetch処理を中断する
  };
},[param]);



4-2.useEffect外でasync関数を定義して呼び出す方法

useEffectの外で定義したasync関数を使う方法もあります。

こちらの書き方は、コードの可読性が高く、関数を再利用する場合におすすめです。

const getData =useCallback(async()=>{//非同期にデータを取得する関数を定義し、useCallbackで参照を安定化
  const res =awaitfetch(param);//リクエストを送信
  setData(await res.json());//取得したデータをstateに渡す
},[param]);//paramが変わらない限り、同じ関数を使いまわす
useEffect(()=>{
  getData();//外部で定義した関数を呼び出す
},[getData]);//paramが変わるとgetDataが再生成され、useEffectも再実行


上記では、「6-2.依存配列にオブジェクトや関数がある」で解説したように、依存配列に関数(今回であれば、getData関数)がある場合、useEffectが余分に実行され、エラーが発生する可能性があります。

そのため、useCallbackを使って、getData関数をメモ化し、レンダーで参照が変わらないようにしています。


こちらも、AbortControllerを使った書き方ができます。


AbortControllerを使った例

const getData =useCallback(async(signal)=>{//引数(signal)は実行キャンセル用シグナル
  try{
    const res =awaitfetch(param,{ signal });//signalプロパティ付きのリクエストを送信
    const json =await res.json();
    setData(json);
  }catch(e){
    if(e.name !=="AbortError"){//キャンセル以外のエラーを拾う
      console.error(e);
    }
  }
},[param]);
useEffect(()=>{
  const ac =newAbortController();//処理を中断するためのインスタンス生成
  getData(ac.signal);
  return()=>{//クリーンアップ
    ac.abort();//実行中の古いfetch処理を中断する
  };
},[getData]);


4-3.即時実行関数式を使う方法

即時実行関数(IIFE)は、定義されるとすぐに実行される関数です。

こちらの記法は、async関数を別に定義しなくていいというメリットがあります。

//IIFEの例
(async()=>{
  //処理内容
})();

参考:IIFE (即時実行関数式) - MDN Web Docs 用語集


IIFEを使った非同期処理の書き方は以下の通りです。

useEffect(()=>{
  (async()=>{//無名のasync関数をIIFEでラップ
    const res =awaitfetch(param);//リクエストを送信
    setData(await res.json());//取得したデータをstateに渡す
  })();
},[param]);


こちらも、AbortControllerを使った書き方ができます。


AbortControllerを使った例

useEffect(()=>{
  const ac =newAbortController();//処理を中断するためのインスタンス生成
  (async()=>{
    try{
      const res =awaitfetch(param,{signal: ac.signal });//signalプロパティ付きのリクエストを送信
      const json =await res.json();
      setData(json);
    }catch(e){
      if(e.name !=="AbortError"){//キャンセル以外のエラーを拾う
        console.error(e);
      }
    }
  })();
  return()=>{//クリーンアップ
    ac.abort();//実行中の古いfetch処理を中断する
  };
},[param]);


5.実際にuseEffectを使ってみよう(タイマーアプリ)

ここでは、以下のような「タイマーアプリ」を使って、useEffectの使い方や仕組みを理解しましょう。



このアプリは、以下のように動作します。


  1. 初回表示時に、APIからタイマーの設定を取得
  2. ローディング画面を消す(リロードすると動作が分かります。)
  3. 開始ボタンでカウントダウンする
  4. カウントが0になったらポップアップを表示する
  5. リセットボタンで初期値に戻す


このタイマーアプリは、useEffectの代表的な使い方がいくつも詰まっています。

コードを通して、「初回レンダリング時の処理」「stateの変化に応じてuseEffectを実行する方法」などを学べます。


なお、useEffect は「何でも1つにまとめて記述」するのではなく、目的ごとに分けて書くのが基本です。

このデモアプリでも、



の3つに分割されています。

このように、「複数のuseEffectを目的別に分ける考え方」もこのタイマーアプリを通じて学べます。


ソースコード


5-1.stateの定義

以下は、タイマーアプリで「扱う状態を定義している部分」だけ抜き出したものです。

ソースコード


今回のタイマーアプリでは、useEffectに加えて、状態管理に「useState」も使用しています。

useStateについては、以下の記事で詳しく解説しているのでぜひご覧ください。


Reactの基礎2|React hook(useState編)


5-2.初回ロード時のuseEffect & ダミーAPI

以下は、初回ロード時に動作するuseEffectと、stateの状態によって、表示されるローディング画面のコードです。

ソースコード


今回のタイマーアプリでは、画面が読み込まれた際にローディング画面が表示されたかと思います。

それは、タイマーアプリが以下のように動作したためです。


  1. コンポーネントが読み込まれる
  2. useStateで管理している「loading」に初期値のtrueがセットされる
  3. 画面がコミットされ、ローディング画面が表示される
  4. 初回ロード時のuseEffectが動く
    1. APIを叩いてデータを取得する
    2. 取得したデータをstateにセットする
    3. ローディング画面を解除するために、「loading」にfalseをセットする
  5. タイマーアプリ本体が表示される
    1. タイマー名に「Wake Up Nihon Tarou.」が表示される
    2. カウント部分に「10」が表示される


今回は、loadingに真偽値を入れて管理しましたが、文字列で管理することで、ローディング中の画面をさらに細かく制御することも可能です。


なお今回は、「3秒後にユーザー設定オブジェクトを返す」というダミーAPIを用意しました。

以下のようなコードで実装しているので、ぜひ参考にしてください。


ダミーAPIのコード


5-3.タイマー動作部

以下は、タイマーを実際に動作させる中心部分のコードを抜き出したものです。

ソースコード


上記のコードの動きをわかりやすくすると、以下のようになります。


  1. コンポーネントがレンダーされる
  2. useEffectが「isRunning」「time」をチェックする
    1. 「!isRunning」:タイマーが停止中なら終了
    2. 「time === null」:初期ロード中なら終了
    3. 「time <= 0」:残り時間が0以下なら終了
  3. 条件をすべて通過したら1秒ごとに実行されるタイマーを作成
  4. timeが変わるとコンポーネントが再レンダー
  5. 再実行前にクリーンアップが走って古いタイマーを削除
  6. 2に戻る


重要な点は、「5」のクリーンアップです。

return()=>clearInterval(id);

この記述がないと、古いタイマーが残り続けてしまい、



といった不具合が発生する場合があります。


5-4.タイマー操作部分

以下は、タイマーアプリの操作部分だけ抜き出したものです。

ソースコード


三項演算子を使って「isRunning」の真偽で開始ボタンと停止ボタンを切り替えており、リセットボタンは常時表示しています。

{!isRunning ?( 開始ボタン ):( 停止ボタン )}


開始ボタン(isRunning = false)

<button
  className="btn btn-start"
  onClick={()=>setIsRunning(true)}//押すとisRunningをtrueにする
  disabled={isRunning}//タイマーが動いてる場合はボタンを押せないようにする
>
  開始
</button>


停止ボタン(isRunning = true)

<button
  className="btn btn-stop"
  onClick={()=>setIsRunning(false)}//押すとisRunningをfalseにする
  disabled={!isRunning}//タイマーが停止中はボタンを押せないようにする
>
  停止
</button>


リセットボタン(常時表示)

//リセット関数
consthandleReset=()=>{
  setTime(initialTime);//timeを初期値に戻す
  setIsRunning(false);//タイマーを停止する
};
//リセットボタン
<button className="btn btn-reset" onClick={handleReset}>
  リセット
</button>


5-5.タイマー終了時のポップアップ

タイマー終了時のポップアップの制御と表示部分のコードは、以下の通りです。

ソースコード


今回は、論理積演算子(&&)による条件付きレンダリングを使いました。

{showPopup &&(ポップアップウィンドウのJSX)}


これは、



というふうに動作します。


6.注意点


6-1.実行タイミング

基本的に、useEffectはレンダーが起こったあとに実行されます。


通常のレンダリング

  1. 画面の更新がトリガーされる
  2. DOM(描画内容)が計算される
  3. 計算結果(DOM)を画面 に反映する
  4. useEffectが発火する


しかし、useEffectがユーザ操作によって発火した場合、画面が描画される前にセットアップ関数が実行されることがあります。

ほとんどのUIはこの挙動で正しく動きますが、以下のように、処理を遅らせたいケースもあります。



こうした場合は、setTimeoutを使って、処理を描画後に遅らせるという対処法もあります。


また、useEffect内でstate(状態)を更新する場合、描画が先に行われるケースがあります。

stateの更新が描画のあとに実行されると、画面がチラついたりフォーカスがうまく機能しなかったりする場合があります。

こうした場合、useEffectの代わりに「useLayoutEffect」を使うことで、stateの更新を描画前に完了させることが可能です。


6-2.依存配列にオブジェクトや関数がある

依存配列の要素に、コンポーネント内で定義されたオブジェクトや関数が含まれている場合、意図しないエフェクトの発火が引き起こされることがあります。

これは、依存配列の変化を「Object.isを使った比較」で検知しているために起こります。


レンダーがトリガーされた際、描画内容を計算するために、コンポーネントが呼び出されます。

このとき、コンポーネント内の関数や変数は再生成されます。

つまりは、メモリ内の別のアドレス(データを置く場所)に、新しく関数や変数が作られるということです。


レンダーが起きたあと、useEffectは依存配列の要素を、古い値(前回のレンダー値)と新しい値で比較(Object.is)します。

JavaScriptのデータ型には、プリミティブ値とオブジェクト値があり、それぞれObject.isの動作が異なります。



このとき、プリミティブ値は、値自体を比較しますが、オブジェクト値は参照を比較します。

そのため、オブジェクト値であるオブジェクトや関数が、依存配列の要素に含まれていると、無限ループや余計な画面読み込みが起こる可能性があります。


参考:JavaScript のデータ型とデータ構造 - JavaScript | MDN

参考:Object.is() - JavaScript | MDN


余計なuseEffectの発火が起きる例


7.不要なuseEffect(アンチパターン)は削除しよう

useEffectは便利ですが、避難ハッチとして用意されたものであるため、多用は避けましょう。

そのほうが、「エラーの発生」「処理速度の低下」といった不具合の発生する可能性が減り、コードの可用性が上がります。


以下のようなケースでは、useEffectを使わずに別のロジックで動作を実現できる場合があります。



8.あとがき

useEffectはエフェクトを管理するためのフックであり、Webアプリケーションを開発する際によく使用します。

本記事で基本的な使い方をきちんとマスターしておきましょう。