Agile育成ブログ
未来を変える喜びを
React

初心者でも簡単にわかるReact useEffect, useLayoutEffectの使い方


Warning: count(): Parameter must be an array or an object that implements Countable in /home/xs638785/agile-software.site/public_html/wp-content/plugins/rich-table-of-content/functions.php on line 490

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の理解の助けにはなると思います。

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

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

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

準備

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

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


import React,{ 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 React,{ 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によるメッセージ

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

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

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

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

useStateで値の更新が行われるとコンポーネントはその値をブラウザ上でも更新する必要があるため再描写(コンポーネントの更新)が行われます。propsが更新された場合も同じようにコンポーネントの再描写が行われます。

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

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

コンポーネントの更新による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 React,{ 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回となります。

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を増やす

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

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

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

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


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 React,{ 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 React,{ 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アップしました”が表示されます。

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

アンマウント処理の追加

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 React,{ 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を設定)

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;

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

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


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によって赤へと変更されます。

useRef

useRefは要素への参照を行うことと値を保持することができる。
値の保持についてはuseStateでも同様のことが可能ですが、useState とは異なりコンポーネントの再描写が行われない。

import { useState } from 'react';

function App() {
  const [name, setName] = useState('');
  const handleOnChange = (e) => setName(e.target.value);

  return (
    <div style={{ margin: '2em' }}>
      <input type="text" value={name} onChange={handleOnChange} />
      <p>名前:{name}</p>
    </div>
  );
}

export default App;

表示されている input 要素にはフォーカスはありません。フォーカスを当てるためには自分でカーソルを input 要素に持っていく必要があります。input 要素にフォーカスが当たると文字列を入力することができ、入力した文字は input 要素の下に表示されます。

useRef の引数には初期値を設定します

ボタンをクリックするとcurrent プロパティに input が入っていることがコンソールにおいて分かります。

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

function App() {
  const [name, setName] = useState('');
  const handleOnChange = (e) => setName(e.target.value)
  const inputEl = useRef(null);
  const handleOnClick = () => inputEl.current.focus();

  return (
    <div className="App">
      <input ref={inputEl} type='text' value={name} onChange={handleOnChange} />
      <p>名前:{name}</p>
      <button onClick={handleOnClick}>inputにフォーカス</button>
    </div>
  );
}

export default App;

useRef を利用することで ref 属性で設定した要素への参照を取得できることがわかります。
参照を利用して DOM にアクセスできるということはアクセスした要素の情報を getBoundingClientRect メソッドを利用して取得することができます。

You cannot copy content of this page