Lesson 5

レンダリング後に処理を実行する

Lesson 5 Chapter 1
useEffectの動作確認

これまでにProps、ルーティング等Reactの基礎を学習してきました。レッスン4ではReactの重要な概念であるStateを学びました。本レッスンではuseEffectについて学習していきます。

useEffectとは

レッスン6で説明しますが、コンポーネントは特定の条件を満たすと再レンダリングされます。再レンダリングとはコンポーネントが変化を検知して処理を頭から実行し直すことです。このとき、毎回実行したい処理もあれば、特定の条件が変化した時のみ実行したい処理もあります。この要求に対応するために、ReactにはuseEffectという機能があります。

レンダリング後の実行

コンポーネントのレンダリング後に毎回実行したい関数があるとき、useEffectを使って以下のように記述します。

useEffect(実行する関数);

実際にレンダリング後に実行されるか簡単な例で確認してみましょう。App.jsxを以下のように編集します。

App.jsx
import { useState, useEffect } from "react"

const App = () => {
                      
  const [isOddNumber, setIsOddNumber] = useState(false);
                      
  useEffect(() => {
      console.log("こんにちは!");
    }
  );
                      
  const onClickButton = () => {
    setIsOddNumber(!isOddNumber);
  };
                          
  return(
    <>
      <p>{isOddNumber?"true":"false"}</p>
      <button onClick={onClickButton}>ボタン</button>
    </>   
  );
                      
};
                      
export {App};

上記はボタンを奇数回クリックするとtrue、偶数回クリックするとfalseを表示するコンポーネントです。これより、ボタンをクリックするたびにfalse→true→false→true→ . . . というように表示が変わりますので、レンダリングされていることを確認できます。useEffectには、コンソールに"こんにちは!"を表示する関数が与えられています。よって、表示が変わるたびにコンソールに"こんにちは!"が表示されるはずです。続いて、App.jsxを表示できるようにindex.jsxを以下のように編集しましょう。

index.jsx
import ReactDOM from 'react-dom/client';
import {App} from './App.jsx'
                      
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <App />
);

Appコンポーネントをindex.jsxにimportし、そのまま出力するように記述しています。ここで、今まで設定していたReact.StrictModeのタグを今回は削除しています。理由は、開発環境下においてはReact.StrictModeは初回のレンダリングを2回実行してしまうためです。コンポーネントのレンダリングとuseEffectに設定した関数の実行の関係性を検証しているので、React.StrictModeが有効だと検証がし難くなるので削除しております。本番環境ではReact.StrictModeを設定していても初回のレンダリングが2回実行されるようなことはありません。それではプログラムを実行してみましょう。ブラウザに以下の画像が表示されると思います。

2023-02-23-14-56-19_.png "false"の文字とボタンが表示されてます。

"false"の文字とボタンが表示されているのが確認できると思います。次にブラウザの表示部で右クリックし、「検証」をクリックしてみましょう。以下の画面が表示されるはずです。

2023-02-23-15-22-57_.png コンソールに"こんにちは!"が表示されています。

右クリックして出てきた表示を開発者ツールといいます。開発者ツールの下の領域がコンソールになります。すでに"こんにちは!"が表示されています。これは初回のレンダリング時に関数が実行されたためです。続いてコンソールを表示したままで、ボタンを数回クリックしてみましょう。以下の画像は5回クリックしたときの画像です。

2023-02-23-15-29-48_.png コンソールに5回分"こんにちは!"の表示が追加されています。

クリックするたびに"こんにちは!"の表示が追加されていくことを確認できると思います。このようにuseEffectに関数を引数として与えると、毎レンダリング後に与えた関数を実行することができます。

初回レンダリング時だけ実行

初回レンダリング時だけ実行したい関数があるとき、useEffectを使って以下のように記述します。

useEffect(実行する関数, []);

前回同様に動作を確認してみましょう。App.jsxを以下のように編集します。

App.jsx
import { useState, useEffect } from "react"

const App = () => {
                      
  const [isOddNumber, setIsOddNumber] = useState(false);
                      
  useEffect(() => {
      console.log("こんにちは!");
    },[]
  );
                      
  const onClickButton = () => {
    setIsOddNumber(!isOddNumber);
  };
                          
  return(
    <>
      <p>{isOddNumber?"true":"false"}</p>
      <button onClick={onClickButton}>ボタン</button>
    </>    
  );
                      
};
                      
export {App};

前回からの変更点はuseEffectの第二引数として、空の配列を追加したのみです。index,jsxは前回のままで実行してみましょう。実行してブラウザのコンソールを表示すると以下の画像の状態になると思います。

2023-02-23-15-45-48_.png コンソールに"こんにちは!"が表示されています。

コンソールに"こんにちは!"が表示されているのが確認できると思います。この状態でボタンを数回クリックしてみましょう。以下の画像は5回クリックしたときの画像です。

2023-02-23-15-47-38_.png コンソールの"こんにちは!"の表示は1つのままです。

false→true→false→ . . . とレンダリングされているにもかかわらず、コンソールに"こんにちは!"の表示は追加されません。このようにuseEffectの第二引数として空の配列を与えると、初回レンダリング時だけ関数を実行することができます。

特定の値変更時に実行

特定の値が変更された時だけ実行したい関数があるとき、useEffectを使って以下のように記述します。

useEffect(実行する関数, [num1, num2, num3, . . . ]);

ここで"num1, num2, num3, . . . "は第一引数の関数を実行するための変数です。このように、関数を実行するための変数は複数設定することが可能です。簡単な例を作成し動作を確認してみましょう。App.jsxを以下のように編集します。

App.jsx
import { useState, useEffect } from "react"

const App = () => {
                      
  const [num1, setNum1] = useState(0);
  const [num2, setNum2] = useState(0);
  const [num3, setNum3] = useState(0);
                      
  useEffect(() => {
      console.log(num1+num2+num3);
    },[num1, num2]
  );
                      
  const addOneToNum1 = () => {
    setNum1(num1+1);
  };
                      
  const addOneToNum2 = () => {
    setNum2(num2+1);
  };
                      
  const addOneToNum3 = () => {
    setNum3(num3+1);
  };
                          
  return(
    <>
      <p>{`num1=${num1}`}</p>
      <button onClick={addOneToNum1}>add one to num1</button>
      <p>{`num2=${num2}`}</p>
      <button onClick={addOneToNum2}>add one to num2</button>
      <p>{`num3=${num3}`}</p>
      <button onClick={addOneToNum3}>add one to num3</button>
    </>    
  );
                      
};
                      
export {App};

今回作成したApp.jsxは初期値に0を持つnum1、num2、num3の3つのStateを定義し、各Stateの現在の値と各Stateに1加えるボタンを表示するコンポーネントです。App.jsxのuseEffectには、第一引数としてnum1、num2、num3の合計値をコンソールに表示する関数、第二引数として[num1, num2]を与えています。index,jsxは前回のままで実行してみましょう。実行してブラウザのコンソールを表示すると以下の画像の状態になると思います。

2023-02-23-16-21-19_.png コンソールに"0"が表示されています。

初回のレンダリングにより、num1、num2、num3の初期値を足した値である"0"がコンソールに表示されます。この状態で"add one to num1"のボタンを押すと以下の状態になります。

2023-02-23-16-39-20_.png "num1=0"から"num1=1"に表示が変わり、コンソールに"1"が表示されます。

"num1=0"から"num1=1"に表示が変わったことから、num1の内容が1に変わるとともにレンダリングされたことが確認できます。このときコンソールには"1"が表示されます。このことから、useEffectの第一引数に設定した関数が実行されているのが確認できます。この状態から"add one to num2"のボタンを押すと以下の状態になります。

2023-02-23-16-39-34_.png "num2=0"から"num2=1"に表示が変わり、コンソールに"2"が表示されます。

"num2=0"から"num2=1"に表示が変わったことから、num2の内容が1に変わるとともにレンダリングされたことが確認できます。このときコンソールには"2"が表示されます。このことから、useEffectの第一引数に設定した関数が実行されているのが確認できます。さらに、この状態から"add one to num3"のボタンを押すと以下の状態になります。

2023-02-23-16-39-47_.png "num3=0"から"num1=3"に表示が変わりますが、コンソールには何も追加されません。

"num3=0"から"num3=1"に表示が変わったことから、num3の内容が1に変わるとともにレンダリングされたことが確認できます。しかしながら、このときコンソールには何も表示されません。useEffectの第一引数に設定した関数が実行されていないことが分かります。念のため、この状態から"add one to num2"のボタンを押した後の状態を確認してみましょう。以下の状態になると思います。

2023-02-23-16-40-00_.png "num2=1"から"num2=2"に表示が変わり、コンソールに"4"が表示されます。

"num2=1"から"num2=2"に表示が変わり、コンソールには"4"が表示されます。useEffectの第一引数に設定した関数が実行されているのが確認できます。このように、useEffectの第二引数に変数を設定すると、初回レンダリング時と設定した変数が変更した時のみ、第一引数の関数が実行されるようになります。

Lesson 5 Chapter 2
useEffectと非同期処理を用いて外部APIからデータを取得する

axiosのインストール

useEffectは特定の条件が満たされたときに、外部APIからデータをダウンロードするといったことにも活用できます。外部APIからデータを取得するライブラリとしてはaxiosがあります。axiosは非同期処理によりWebサーバーにHTTPリクエストを送信するメソッドを提供します。HTTPは、Webサーバーから情報を取り出したり、Webサーバーへ情報を送ったりするために決められた通信の決まり事のことです。axiosはターミナルにおいて以下を実行することでのインストールできます。

npm install axios

以降では、useEffectとaxiosのgetメソッドを使用して外部APIからデータを取得する方法を学んでいきます。

JSONPlaceholderからデータを取得する

データ送受信のテストを行えるサービスにJSONPlaceholderがあります。JSONPlaceholderは登録不要かつ無料で使用することができます。サイトのURLはhttps://jsonplaceholder.typicode.com/ になります。サイトを覗いてみましょう。ブラウザで以下の画面が表示されると思います。

2023-02-24-12-09-29_.png JSONPlaceholder

サイトの下の方に"Resources"という項目があり、そのリストに"/users"というリンクがあると思います。今回はこのリンク先のデータを取得したいと思います。データのURLはhttps://jsonplaceholder.typicode.com/users です。データの内容は以下のようになります。

[
{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
    }
},
{
  "id": 2,
  "name": "Ervin Howell",
  "username": "Antonette",
  "email": "Shanna@melissa.tv",
  "address": {
    "street": "Victor Plains",
    "suite": "Suite 879",
    "city": "Wisokyburgh",
    "zipcode": "90566-7771",
    "geo": {
      "lat": "-43.9509",
      "lng": "-34.4618"
    }
  },
  "phone": "010-692-6593 x09125",
  "website": "anastasia.net",
  "company": {
    "name": "Deckow-Crist",
    "catchPhrase": "Proactive didactic contingency",
    "bs": "synergize scalable supply-chains"
  }
},
                      
・
・
・
(途中省略)
・
・
・
                      
 ,
{
  "id": 10,
  "name": "Clementina DuBuque",
  "username": "Moriah.Stanton",
  "email": "Rey.Padberg@karina.biz",
  "address": {
    "street": "Kattie Turnpike",
    "suite": "Suite 198",
    "city": "Lebsackbury",
    "zipcode": "31428-2261",
    "geo": {
      "lat": "-38.2386",
      "lng": "57.2232"
    }
  },
  "phone": "024-648-3804",
  "website": "ambrose.net",
  "company": {
    "name": "Hoeger LLC",
    "catchPhrase": "Centralized empowering task-force",
    "bs": "target end-to-end models"
  }
}
]

全て記述すると長いので途中省略しましたが、取得するデータは10件分の個人情報を記述したオブジェクトを要素として持つ配列になります。このデータをuseEffectを使用して初回レンダリング時に取得してみたいと思います。App.jsxを以下のように編集しましょう。

App.jsx
import { useEffect } from "react";
import axios from "axios";
                      
const App = () => {
                      
  useEffect(()=>{axios.get('https://jsonplaceholder.typicode.com/users')
      .then((response)=>{
        console.log(response);
      })
    }, []
  );
                          
  return(
    <p>データを取得しています。</p> 
  );
                      
};
                      
export {App};

App.jsxにおける要点を説明したいと思います。最初に、以下の記述によりreactのライブラリからuseEffectを、axiosのライブラリからaxiosをimportしています。

import { useEffect } from "react";
import axios from "axios";

続いてApp関数の中を見てみましょう。重要な個所は以下になります。

  useEffect(()=>{axios.get('https://jsonplaceholder.typicode.com/users')
    .then((response)=>{
      console.log(response);
    });
  }, []
);

useEffectが記述されています。初回レンダリング時に実行することから第二引数は空の配列になっています。第一引数の関数を見ると、中身が"axios.get(' . . . ').then( . . . )"と記述されているのが分かります。引数の内容は以下を意味します。

axios.get('データ取得先のURL').then(データ取得後の処理);

"axios.get('データ取得先のURL')"で関数の引数に指定したURLにデータ取得のためのリクエストを送信します。データの取得に成功すると"then(データ取得後の処理)"で引数として与えた処理が実行されます。このときの処理は取得したデータを引数(今回の場合は"response")とする関数で記述します。今回は処理内容が"console.log(response)"であることから、取得したデータをコンソールに表示することだと分かります。このように関数の実行時に実行結果が決まらず、実行結果が決まった段階で結果に応じた処理が行われることを非同期処理といいます。

実行結果を確認します。プログラムを実行し、ブラウザを立ち上げコンソールを表示させた画像が以下になります。

2023-02-24-16-48-28_.png "データを取得しています。"が表示され、コンソールに取得したデータが表示されています。

ブラウザに"データを取得しています。"の表示を確認できると思います。また表示しているコンソールには"{data: Array(10), . . . "と記述されていて、10要素の配列を取得できているのが確認できます。配列の中身を確認すると、https://jsonplaceholder.typicode.com/usersの内容が取得できているので、確認してみてください。

エラー処理の実装

先ほどは"axios.get( . . . )"がデータ取得に成功した時の処理のみ記述しましたが、通信状況によっては必ずしもデータを取得できるとは限りません。データ取得に失敗した時のために、エラー処理を実装したいところです。"axios.get( . . . )"はPromiseインスタンスというオブジェクトを返しますが、これは非同期処理が成功か失敗によってその後の処理を分岐することができます。この分岐を利用することでエラー処理の実装が可能となります。Promiseインスタンスによる処理の分岐の仕方は以下になります。

Promiseインスタンス.then(非同期処理が成功した時の処理).catch(非同期処理が失敗した時の処理);

Promiseインスタンスを返す関数の非同期処理が成功した場合はthenに記述された処理を、失敗した場合にはcatchに記述された処理を実行します。処理は関数で記述します。例えば以下のような形です。

func
.then((resolve)=>{console.log(response);})
.catch((error)=>{console.error(error);})

この例ではfuncがPromiseインスタンスであり、"(resolve)=>{console.log(response);}"が成功時の処理、"(error)=>{console.error(error);}"が失敗時の処理になります。ここで引数"resolve"はfuncの成功時の実行結果であり、引数"error"はfuncの失敗時のエラーメッセージになります。

以上を踏まえて、エラー処理を実装したApp.jsxは以下のようになります。

App.jsx
import { useState, useEffect } from "react";
import axios from "axios";
                      
const App = () => {
                      
  const [isSuccess, setIsSuccess] = useState(false);
                      
  useEffect(()=>{axios.get('https://jsonplaceholder.typicode.com/users')
    .then((response)=>{
      console.log(response);
      setIsSuccess(true);
    }).catch((error)=>{
      console.error(error);
    })
    }, []
  );
                          
  return(
    <p>{isSuccess?"データ取得成功":"データ取得失敗"}</p>
  );
                      
};
                      
export {App};

データ取得の成否が分かりやすいように、今回はisSuccessというStateを定義しました。isSuccessの初期値にはfalseを設定しています。isSuccessによって、return文で表示するメッセージの内容を成功の場合に"データ取得成功"、失敗の場合に"データ取得失敗"にと切り替えられるようにしています。またuseEffectの内容を以下のように変更しています。

useEffect(()=>{axios.get('https://jsonplaceholder.typicode.com/users')
  .then((response)=>{
    console.log(response);
    setIsSuccess(true);
  }).catch((error)=>{
    console.error(error);
  })
  }, []
);

axios.get関数が成功した場合、コンソールに取得結果を表示して、isSuccessをtrueに更新します。失敗の場合、コンソールにエラーメッセージを表示します。ブラウザで結果を確認してみましょう。以下の画像はaxios.get関数が成功した場合の表示です。

2023-02-24-13-38-57_.png "データ取得成功"のメッセージが表示されています。

ブラウザの表示部には"データ取得成功"のメッセージが、コンソールには取得したデータが表示されているのが確認できます。以下の画像はaxios.get関数が失敗した場合の表示です。

2023-02-24-16-36-39_.png "データ取得失敗"のメッセージが表示されています。

ブラウザの表示部には"データ取得失敗"のメッセージが、コンソールにはエラーメッセージが表示されているのが確認できます。以上がaxiosのエラー処理の実装方法になります。

別の表現によるaxiosの使用方法

ところで、今回扱った非同期処理は"async"と"await"というものを使った別の記述方法もあります。よく使用される書き方なので、こちらでApp.jsxを書き換えたコードについても紹介したいと思います。"async"と"await"を使ってApp.jsxを書き換えると以下になります。

App.jsx
import { useState, useEffect } from "react";
import axios from "axios";
                      
const App = () => {
                      
  const [isSuccess, setIsSuccess] = useState(false);
                      
  useEffect(()=>{(async () => {
    try{
      const response = await axios.get('https://jsonplaceholder.typicode.com/users');
      console.log(response);
      setIsSuccess(true);
    }catch(error){
      console.error(error);
    }
  })()
  }, []
  );
                          
  return(
    <p>{isSuccess?"データ取得成功":"データ取得失敗"}</p>
  );
                      
};
                      
export {App};

前回からの変更点は以下の部分になります。

useEffect(()=>{
  (async () => {
    try{
      const response = await axios.get('https://jsonplaceholder.typicode.com/users');
      console.log(response);
      setIsSuccess(true);
    }catch(error){
      console.error(error);
    }
  })()
  }, []
);

括弧が多いため分かりにくいですが、useEffectの第一引数の関数の中身("()=>{ . . . }"の" . . . "の部分)に即時関数が記述されています。即時関数として実行する関数は"async () => {}"という形式になっています。これは非同期処理を含む関数という意味になります。なぜこのような回りくどい記述にする必要があるかを説明します。まず、即時関数を使うのは非同期処理を含む関数を定義と同時に実行させるためです。またこの非同期処理を含む即時関数をuseEffectの第一引数に直接記述せず、関数の中に記述するのは、useEffectの第一引数に非同期処理を含む関数を直接記述できないためです。続いて"async () => { . . . }"の" . . . "に記述された内容について見てみましょう。

try{
  const response = await axios.get('https://jsonplaceholder.typicode.com/users');
  console.log(response);
  setIsSuccess(true);
}catch(error){
  console.error(error);
}

"try{ . . . }catch(error){ . . . }"という構文が使用されています。この構文ではまずtryの内容を実行し、エラーを検知した段階でcatchの内容に実行が移ります。tryの内容を見てみると、最初に"const response = await axios.get( . . . );"が記述されています。非同期処理は実行開始と結果が決まるタイミングに時間差があり、通常は結果を待たずに後の処理が先に実行されてしまいますが、このように非同期処理にawaitを付与することで処理が完了するまで、その後の処理の実行を待つことができます。"await axios.get();"の戻り値responseには非同期処理が成功した時の結果が格納されます。"axios.get()"の実行が成功すれば、実行結果をコンソールに表示し、isSuccessをtrueに更新します。一方、"axios.get()"が失敗するとエラーを検知しcatchの内容に処理が移ります。catchではコンソールにエラーメッセージを表示します。ここでcatchの引数のerrorはエラーメッセージになります。

記述は異なりますが、結果は前回のApp.jsxと同じになります。非同期処理について、両方の書き方ともよく使用されるので覚えておきましょう。