本文書では、ReactのHookの中でもuseStateの次に使用頻度も高く重要なuseEffectの使用方法について説明を行なっています。

シンプルなコードを利用しているので本文書を読み終えるとuseEffectの基本的な使用方法とuseEffectを利用した外部からのデータ取得方法、コンポーネントのライフサイクルとの関係を理解することができます。またuseEffectと非常によく似た機能を持つuseLayoutEffectとの違いも理解することができます。

useEffectの動作確認のためにReack HookのuseStateを利用するのでuseStateの知識が必要になります。

React useEffectとは

useEffectは関数(Functional)コンポーネントのみで利用することができるHookです。useEffectのEffectは”Side Effect”(副作用)を意味しています。Side Effectにはfetch関数を利用して外部のリソースからデータを取得したり、DOMの更新、ロギング(console.logも含む)などの処理が含まれます。

useEffectを理解する上でclassコンポーネントに関する機能との比較は重要ではないかもしれませんが、useEffectはReact ClassコンポーネントのライフサイクルcomponentDidMount, componentDidUpdateとcomponentWillUnmountの3つと同様な処理を行うことができるHookです。

componetDidMountはコンポーネントのマウント直後に実行され、componentDidUpdateはコンポーネントが再レンダリングされる度に実行され、comonentWillUnmountはコンポーネントがアンマウントされて破棄される直前に実行されるライフサイクルフックです。

最近Reactを勉強し始めた人の場合、React Classコンポーネントを利用せずFunctionalコンポーネントのみでアプリケーションを開発できるためClasssコンポーネントについて説明を行ってもわからないと思います。その場合はClassコンポーネントとの比較は無視してください。しかし、ライフサイクルフックにつけられている名前componentDidMountなどはuseEffectとは異なり名前からどのような処理かイメージすることができるのでuseEffectの理解の助けにはなると思います。
fukidashi

useEffectを利用することでコンポーネントの内容を表示する際に外部のサーバからAPIを経由してデータを取得することやコンポーネントが更新する度に別の処理を実行するということが可能になります。またuseEffectは一つのコンポーネントに複数記述することも可能です。

そもそもライフサイクルって何?という人でもわかるようにシンプルな例を使ってuseEffectの使用方法を確認していきます。

useEffectは画面がレンダリングされた後に実行されるということが重要なのでそれもしっかりと覚えておいてください。レンダリング前に何か処理をしたいという時に利用することはできません。レンダリング前に何か処理を行いたいという場合にはuseLayoutEffect Hookを確認してください。
fukidashi

準備

useEffectの動作確認を行うためにそのベースになるコードを作成します。

useStateを使ってcountのstate変数を設定しボタンをクリックする度にCount数が増えるコードを作成します。


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

function App() {

  const [count, setCount] = useState(0)

  return (
    <div className="App">
      <h1>Learn useEffect</h1>
      <h2>Count: { count }</h2>
      <button onClick={() => setCount(count+1)}>+</button>
    </div>
  );
}

export default App;

作成後、ブラウザを開くとCountの初期値は0ですが表示されている+ボタンをクリックするとCount数が増えることが確認できます。

Count数が増えることを確認
Count数が増えることを確認

useEffectの動作確認

この時点でuseEffectがどのようなものかわからなくても安心してください。ここから使い方を説明していきます。

初めてのuseEffect

useEffectを利用する場合は、useStateと同様にuseEffectをimportする必要があります。useEffectを利用してコンソールログに文字列を表示するように先程のコードの更新を行います。


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

function App() {

  const [count, setCount] = useState(0)

  useEffect(()=>{
    console.log('useEffectが実行されました')
  })

  return (
    <div className="App">
      <h1>Learn useEffect</h1>
      <h2>Count: { count }</h2>
      <button onClick={() => setCount(count+1)}>+</button>
    </div>
  );
}

export default App;

console.logの中身を確認するためにブラウザのデベロッパーツールを開いてください。useEffectの中にconsole.logを追加しブラウザをリロードすることでコンソールログにuseEffectのconsole.logの中で記述したメッセージが表示されることが確認できます。

useEffectによるメッセージ
useEffectによるメッセージ

React 18の場合はコンソールに”useEffectが実行されました”が2回表示されます。src/index.jsにある<React.StrictMode>のタグをコメントすると表示は1回になります。

useEffectを追加しましたが、useEffectを実行させるような処理をコードに記述していないのでuseEffectはコンポーネントが表示される流れの中で自動で実行されていることがわかるかと思います。
fukidashi

次に”+”ボタンを3回クリックしてください。ブラウザ上ではCountの数が3になり、コンソールログを見ると”useEffectが実行されました”のメッセージが4になっていることが確認できます。

コンソールログの確認
コンソールログの確認

ここまでの動作確認で、useEffectはブラウザでコンポーネントが初めて表示される時に必ず一度実行されること、Countの更新によりコンポーネントが再レンダリングされる度に実行されることがわかりました。

useStateで値の更新が行われるとコンポーネントはその値をブラウザ上でも更新する必要があるため再レンダリング(コンポーネントの更新)が行われます。
fukidashi

useEffectがコンポーネントの初期化中に必ず一度実行されるということはuseEffectを利用して事前に外部からAPIでデータを取得することでコンポーネントの初期化の流れの中でデータを表示させることができます。つまりページを開いた直後にボタンをクリックしたり何かの操作をすることなしで外部から取得したデータを表示することできます。useEffectを利用したデータの取得方法と表示方法については後ほど実際のコードを使って説明します。

最初の1回の表示直後に行われるuseEffectの実行がライフサイクルのcomponentDidMountに対応し、それ以降のuseEffectの実行はcomponentDidUpdateに対応します。
fukidashi

コンポーネントの更新によるuseEffectの停止

useEffectは必ず一度何もしなくても実行されることとコンポーネントが更新される度にuseEffectが実行されることがわかりました。しかし、コンポーネントの更新の度にuseEffectの中の処理が必要ではない場合もあります。そのような場合は、コンポーネントの更新によるuseEffectを止めることが可能です。その場合はuseEffectの第2引数に[]を追加します。


useEffect(()=>{
  console.log('useEffectが実行されました')
},[])

[]を追加後にブラウザを使って+ボタンをクリックしてCount数を増やしてください。コンポーネントが表示される最初の一回のuseEffectは実行されますが、ボタンをクリックしてコンポーネントを更新してもuseEffectは実行されなくなりました。

useEffectによるメッセージ
useEffectによるメッセージ

state変数によるuseEffectの実行

useEffectに空の配列を設定することでコンポーネントの更新によるuseEffectの実行を止めることが確認できました。しかし場合によってはある特定の変数であるstateの更新があった時だけuseEffectを実行したいという場合があります。その場合は追加した空の配列に変数stateを追加することでその変数の変化のみを監視しuseEffectを実行させることができます。

動作確認を行うために新たにstate変数count2を追加します。useEffectの配列にはcountだけを追加しています。つまりcountだけを監視し、この変数が更新されるとuseEffectが実行されます。


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

function App() {

  const [count, setCount] = useState(0)
  const [count2, setCount2] = useState(0)

  useEffect(()=>{
    console.log('useEffectが実行されました')
  },[count])

  return (
    <div className="App">
      <h1>Learn useEffect</h1>
      <h2>Count: { count }/ Count2: { count2 }</h2>
      <button onClick={() => setCount(count+1)}>Count+</button><br/>
      <button onClick={() => setCount2(count2+1)}>Count2+</button><br/>
    </div>
  );
}

export default App;

ブラウザを開いてCount+ボタンを3回、Count2+ボタンを5回クリックします。

追加したCount2
追加したCount2

Countボタンを押す時のみuseEffectが実行されるのでコンソールログには4回のメッセージが表示されます。

count更新によるメッセージ回数
count更新によるメッセージ回数
最初の1回はコンポーネントが表示される際のメッセージのため4回となります。
fukidashi

useEffectの[]配列の利用方法を理解することができました。

useEffect内でcount数を増やした場合の動作

setCount, setCount2を使ってuseEffect内でcount数を増やすとどのような動作になるのか確認しておきましょう。


useEffect(()=>{
  console.log('useEffectが実行されました')
  setCount(count+1)
  setCount2(count2+1)
},[count])

countのみ配列に追加しておいてもどちらのstate変数も増加し続けることがわかります。この場合はcountというブラウザ上で表示している値のためuseEffectが動作し続けていることを目視で確認できるため予想外の処理かどうかすぐに判断することができます。しかし、ブラウザ上では表示されないFetchなどの処理の場合には適切に[]の設定を行わないとuseEffectがバックグランドで動作し続けるという問題が発生します。useEffectを利用する場合は[]の設定に注意が必要です。

useEffect内でcountを増やす
useEffect内でcountを増やす

state変数をオブジェクトに変更した場合

useStateでname変数を定義し、firstNameとlastNameプロパティで構成されたオブジェクトを設定します。”John”, “Doe”のボタンを設定し、ボタンをクリックするとname変数のfirstNameとlastNameのどちらかが更新されるように設定します。


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

function App() {
  const [name, setName] = useState({
    firstName: '',
    lastName: '',
  });

  const handleFirstName = (firstName) => {
    setName((prev) => ({ ...prev, firstName }));
  };

  const handleLastName = (lastName) => {
    setName((prev) => ({ ...prev, lastName }));
  };

  return (
    <div className="App">
      <h1>Learn useEffect</h1>
      <h2>Name:{`${name.firstName} ${name.lastName}`}</h2>
      <div>
        <button onClick={() => handleFirstName('John')}>John</button>
        <button onClick={() => handleLastName('Joe')}>Doe</button>
      </div>
    </div>
  );
}

export default App;

“John”, “Doe”のボタンをクリックするとブラウザには”John Doe”が表示されます。

ボタンをクリックした場合
ボタンをクリックした場合

依存配列にnameを設定した場合にuseEffectの処理がどのように動作するか確認します。


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

function App() {
  const [name, setName] = useState({
    firstName: '',
    lastName: '',
  });

  const handleFirstName = (firstName) => {
    setName((prev) => ({ ...prev, firstName }));
  };

  const handleLastName = (lastName) => {
    setName((prev) => ({ ...prev, lastName }));
  };

  useEffect(() => {
    console.log('useEffectが実行されました');
  }, [name]);

  return (
    <div className="App">
      <h1>Learn useEffect</h1>
      <h2>Name:{`${name.firstName} ${name.lastName}`}</h2>
      <div>
        <button onClick={() => handleFirstName('John')}>John</button>
        <button onClick={() => handleLastName('Joe')}>Doe</button>
      </div>
    </div>
  );
}

export default App;

“John”ボタンを1回目クリックした場合に”useEffectが実行されました”が表示されますがさらにボタンをクリックしても”useEffectが実行されました”のメッセージがコンソールに表示されます。firstNameの値は変更がなくてもオブジェクトの場合はuseEffectの中の処理が実行されます。”Doe”をクリックしても同様です。”firstName”ボタンをクリックした時のみ実行させたい場合には下記のように設定する必要があります。


  useEffect(() => {
    console.log('useEffectが実行されました');
  }, [name.firstName, name.lastName]);

JavaScriptの場合はオブジェクトの中身が一緒でもオブジェクトに割り当てられる参照が変数に保存されるため同一のものと判断されないためです。


name =  {
    firstName:'John',
    lastName:'Doe'}
{firstName: 'John', lastName: 'Doe'}
name2 = {
    firstName:'John',
    lastName:'Doe'}
{firstName: 'John', lastName: 'Doe'}
name === name2
false

useEffectでの外部からデータをfetchする方法

useEffectはコンポーネントの初期化中のマウント後に外部からデータを取得する際に利用することができます。実際に外部のサーバを利用してどのようにデータを取得するために設定を行うのか確認をしておきます。

外部のリソースは、JSONPLACEHOLDERを利用してfetchメソッドでpostsデータを取得します。JSONPlaceHolderは指定したURLにアクセスするとダミーのJSONデータを戻してくれるサービスで無料で利用することができます。

外部のサーバのやりとりにuseEffectを利用する場合は第2引数に[](配列)をつけるのを忘れないでください。配列をつけていない場合はコンポーネントの更新によりuseEffectが実行され、サーバへのFetchが継続して行われることになりサーバへの負荷をかけることになります。ネットワーク処理がブラウザ上では気づかない場合があるのでブラウザ上ではなにも発生していないのにすごい数のリクエストがサーバに送信されていることがあります。
fukidashi

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

function App() {

  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then(response => response.json())
      .then(data => {
        setPosts(data)
      },[])
  })

  return (
    <div className="App">
      <h1>Learn useEffect</h1>
      <div>
        {
          posts.map(post => (
            <div key={post.id}>{post.title}</div>
          ))
        }
      </div>
    </div>
  );
}

export default App;

useStateを使ってstata変数postsを定義し、初期値を空の配列に設定します。マウント後にuseEffect内のfetchメソッドが実行され、JSONPLACEHOLDERのURLにアクセスを行いpostsデータ一覧を取得します。取得したデータはsetPostでposts変数に挿入します。最後にmapメソッドで展開し、ブラウザに一覧を表示しています。

useEffectを利用すると外部からデータを取得し表示させることができることが確認できました。

postのリスト一覧
postのリスト一覧

async, awaitを利用して外部から取得する場合

useEffectでasync, awaitを利用して外部リソースからデータを取得したい場合は下記のように記述することができます。表示される内容は先ほどと同じです。


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

function App() {

  const [posts, setPosts] = useState([]);

  useEffect(() => {
    const fetchPost = async () => {
      const response = await fetch(
        'https://jsonplaceholder.typicode.com/posts'
      );
      const posts = await response.json();
      setPosts(posts);
    };
    fetchPost();
  })

  return (
    <div className="App">
      <h1>Learn useEffect</h1>
      <div>
        {
          posts.map(post => (
            <div key={post.id}>{post.title}</div>
          ))
        }
      </div>
    </div>
  );
}

export default App;

コンポーネントのアンマウント時の処理

ここまでの動作確認で、useEffectではコンポーネントのライフサイクルの流れの中で自動で実行されることがわかり、componentDidMount, componentDidUpdateと同様の処理が行えることが確認できました。

最後にコンポーネントのアンマウント時に実行されるcomponentWillUnmountと同様の処理について説明を行なっていきます。

新しいコンポーネントCount.jsの作成

コンポーネントのアンマウント時に行う処理の動作確認を行うために新たにCountコンポーネントを追加します。ファイルはcomponentsディレクトリの下にCount.jsファイルを作成します。

中身はuseStateでstate変数countを追加し、useEffectでコンポーネントのマウント後にsetIntervalを使って1秒ごとにcountを1つアップするといったものです。


import { useEffect, useState } from 'react';

function Count() {

    const [count, setCount] = useState(0)

    useEffect(() => {
        console.log('useEffectが実行されました')

        setInterval(() => {
            setCount(count => count + 1);
            console.log('カウントが1アップしました')
          }, 1000);

    },[])

    return (
      <div>
          <h1>Count: {count}</h1>
      </div>
    );
  }
  
  export default Count;

メインのApp.jsファイルからCountコンポーネントをimportして追加します。


import { useState } from 'react';
import './App.css';
import Count from './components/Count.js'

function App() {

  return (
    <div className="App">
      <h1>Learn useEffect</h1>
      <Count/>
  );
}

export default App;

ブラウザで確認するとSetIntervalを設定しているので、1秒ごとにCountが1アップします。

Countが1秒ごとにアップ
Countが1秒ごとにアップ

useEffectは一度だけ実行され、コンソールログにはCountが増える度に”カウントが1アップしました”が表示されます。

コンソールログの表示
コンソールログの表示

setCount関数の中でprevious Valueを利用しない場合はブラウザ上のカウント数は1のまま増えません。


import { useEffect, useState } from 'react';

function Count() {

    const [count, setCount] = useState(0)

    useEffect(() => {
        console.log('useEffectが実行されました')

        setInterval(() => {
            setCount(count + 1);
            console.log('カウントが1アップしました')
          }, 1000);

    },[])

    return (
      <div>
          <h1>Count: {count}</h1>
      </div>
    );
  }
  
  export default Count;

アンマウント処理の追加

Countコンポーネントをアンマウントする際の処理を追加します。useEffectの中にreturnを追加し処理を設定することでアンマウント時にその処理が行われます。ここではconsole.logでメッセージを表示させます。


useEffect(() => {
    console.log('useEffectが実行されました')

    setInterval(() => {
        setCount(count => count + 1);
        console.log('カウントが1アップしました')
        }, 1000);

    return () => {
        console.log('コンポーネントがアンマウントしました')
    }
},[])

ここまでの設定では、ブラウザをリロードしてもアンマウント処理時のメッセージを表示させることはできません。アンマウントの処理を確認するために親側のApp.jsを利用して、Countコンポーネントの表示/非表示を制御します。

Toggle処理の追加

Countコンポーネントの表示/非表示を切り替えるためにApp.jsファイルにuseStateでstate変数displayを追加します。またToggleボタンを追加し、onClickイベントでdisplay変数の値を切り替えます。


import { useState } from 'react';
import './App.css';
import Count from './components/Count.js'

function App() {

  const [display, setDisplay] = useState(true)

  return (
    <div className="App">
      <h1>Learn useEffect</h1>
      <button onClick={()=>setDisplay(!display)}>Toggle</button>
      {display && <Count/>}
    </div>
  );
}

export default App;

Countコンポーネントをdisplayがtrue, falseで表示・非表示に切り替えるために下記を追加しています。


{ display && <Count/> }

ブラウザで確認するとToggleボタンが表示され、1秒ごとにCountがアップします。

Toggleボタンが表示された状態
Toggleボタンが表示された状態

ToggleボタンをクリックするとCountコンポーネントが非表示になります。

Toggleボタンをクリック後
Toggleボタンをクリック後

コンソールを確認するとコンポーネントが非表示になり、アンマウントされたので、”コンポーネントがアンマウントしました”のメッセージが表示されますが、Warningが発生し、memory leakについてのエラーメッセージが記述されています。またコンポーネントが消えたのにも関わらずカウントアップのメッセージ(カウントが1アップしました)は継続して表示されていることが確認できます。

コンソールログにエラー
コンソールログにエラー

クリーンアップ処理の追加

この問題を解決するためにアンマウント処理の中でsetIntervalをクリーンアップする処理を追加します。


useEffect(() => {
    console.log('useEffectが実行されました')

    const interval = setInterval(() => {
        setCount(count => count + 1)
        console.log('カウントが1アップしました')
        }, 1000)

    return () => {
        clearInterval(interval)
        console.log('コンポーネントがアンマウントしました')
    }
},[])

setIntervalをinterval変数に設定し、returnの中でclearIntervalを実行しています。

再度ブラウザでToggleボタンを押してCountコンポーネントを非表示にすると先程のwarningメッセージも表示されず、アンマウントしたメッセージが表示されます。

アンマウントメッセージ
アンマウントメッセージ

このようにuseEffectではマウント時に実行した処理をアンマウント時に解除する処理が必要となることを覚えていてください。setIntervalだけではなくイベントの設定なども解除を忘れないとアンマウントしてもイベントが削除されず残ったままになってしまいます。(addEventListenerと実行したらremoveEventListenerを設定)

fetchのクリーンアップ処理

setIntervalをuseEffectのクリーンアップ処理で解除する処理の動作確認を行いましたが、fetchが実行されデータを取得途中にページを移動した場合やコンポーネントがアンマウントされた場合にはfetch処理は継続されたままになります。

fetchの処理を中断したい場合にAbortControllerを利用することができます。AbortControllerはReactの機能ではありません。


import { useState, useEffect } from 'react';

function Post() {
  const [posts, setPosts] = useState([]);

  const controller = new AbortController();
  const signal = controller.signal;

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/posts', { signal })
      .then((response) => response.json())
      .then((data) => {
        setPosts(data);
      }, []);

    return () => {
      controller.abort();
    };
  });
  return (
    <>
      <h1>Post一覧</h1>
      {posts.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </>
  );
}

export default Post;

AbourControllerからcontrollerオブジェクトを作成します。controllerオブジェクトはsignalプロパティとabortメソッドを持っており、signalオブジェクトをfetch関数のオプションに設定し、abortメソッドはuseEffectのクリーンアップ処理に設定します。

コンポーネントのアンマウント時にクリーンアップ処理によりabortメソッドが実行されfetch関数が中断されます。

動作確認をするために下記のようにToggleボタンをクリックすることでfetch関数の途中でアンマウントを行いfetch関数を中断することができます。


import { useState } from 'react';
import './App.css';
import Post from './components/Post';

function App() {
  const [display, setDisplay] = useState(true);
  return (
    <div className="App">
      <button onClick={() => setDisplay(!display)}>Toggle</button>
      {display && <Post />}
    </div>
  );
}

export default App;

Toggleボタンをクリックするとコンソールには”Uncaught (in promise) DOMException: The user aborted a request.”メッセージが表示されます。

useLayoutEffect

ReactのHookにはuseEffectと表示に似た名前とuseLayoutEffectというHookがあります。useEffectとuseLayoutEffectは使い方、記述方法については似ているのですが大きな違いがあります。

useEffectは一度画面が描写された後にuseEffectの中の処理が実行されます。useLayoutEffectは画面が描写される前にuseLayoutEffectの中の処理が実行されます。

コードを利用して違いを確認してみましょう。まず下記のようにuseEffectを利用してコードを記述します。


import { useState, useEffect } from 'react';

function App() {
  const [count, setCount] = useState(99999);

  useEffect(() => {
    setCount(0);
  }, []);
  return <div>{count}</div>;
}

export default App;

ブラウザで動作確認すると一瞬99999が表示された後に0が表示されます。これは先程説明した通りuseEffectでは一度画面が描写された後にuseEffectの中のコードが実行されるためこのような動作になります。


import { useState, useLayoutEffect } from 'react';

function App() {
  const [count, setCount] = useState(99999);

  useLayoutEffect(() => {
    setCount(0);
  }, []);
  return <div>{count}</div>;
}

export default App;

useEffectからuseLayoutEffectに書き変えます。useLayoutEffctでは画面が描写される前に実行されるため初期値である99999が画面に表示されることはなく0が表示されます。

次の例ではdiv要素にアクセスを行い背景色を変更することで違いを確認します。div要素へのアクセスにはuseRefを利用して背景色を赤に変更しています。


import { useState, useEffect, useRef } from 'react';

function App() {
  const [count, setCount] = useState(99999);
  const divElement = useRef();

  useEffect(() => {
    setCount(0);
    const element = divElement.current;
    element.style.backgroundColor = 'red';
  }, []);
  return (
    <div ref={divElement} style={{ backgroundColor: 'blue' }}>
      {count}
    </div>
  );
}

export default App;

デフォルトではstyle属性でblueに設定しているdiv要素の背景色は青になります。useEffectでは一度描写が行われので一色背景色が青になりますがuseEffectによって赤へと変更されます。

useEffectからuseLayoutEffectに変更を行います。


import { useState, useLayoutEffect, useRef } from 'react';

function App() {
  const [count, setCount] = useState(99999);
  const divElement = useRef();

  useLayoutEffect(() => {
    setCount(0);
    const element = divElement.current;
    element.style.backgroundColor = 'red';
  }, []);
  return (
    <div ref={divElement} style={{ backgroundColor: 'blue' }}>
      {count}
    </div>
  );
}

export default App;

useLayoutEffectでは画面が描写される前に実行されるため背景色が青の画面が表示されることはなく赤の背景色の画面が表示されます。

複数のuseEffect

useEffectは1つの関数の中に複数記述することも可能です。useEffectとuseLayoutEffectを一緒に利用することも可能です。


function App() {

  useLayoutEffect(() => {
    //処理A
  }, []);

  useEffect(() => {
    //処理B
  }, []);

  useEffect(() => {
    //処理C
  }, []);

まとめ

useEffect, useLayoutEffectを学ぶことでコンポーネントを表示・非表示する際は急にコンポーネントが現れたり消えたりするわけではなくコンポーネントの内容を表示させる時はマウント処理、非表示にする際にはアンマウント処理といった処理が行われることがわかりました。さらにuseEffect, useLayoutEffectの違いからマウント処理の中でもさらに細かい処理が行われていることもわかりました。