クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【React】フックとjson-serverをFetch APIで連携(Reactライフサイクルの理解)

Reactの続きです。前回はフック(React Hooks)を事始めました。基本のフックuseState、useEffect、useContextを体験しました。今回はこのフックにFetch APIを使ってjson-serverのデータベースを連携してみました。最初、処理実行のタイミングがうまくいかず、試行錯誤の過程でReactのライフサイクルについて理解が深まりました。それではいきましょう!

【目次】

※参考:前回記事
【React】フック(React Hooks)事始め:useState、useEffect、useContext - クモのようにコツコツと

※参考:Reactを習得するためにやったことまとめ
qiita.com

前回のおさらい

フック(React Hooks)を事始める。基本のフックuseState、useEffect、useContextの理解。

https://cdn-ak.f.st-hatena.com/images/fotolife/i/idr_zz/20210216/20210216052230.jpg

※参考:【React】フック(React Hooks)事始め:useState、useEffect、useContext - クモのようにコツコツと

今回はこのフックに外部のデータベースの値を初期値として読み込みたい。

importでJSONファイルを読み込む

前回、データの初期値を「App.js」の変数dataに入れていた。

const data = {
  name: '羊',
  count: 0
};

まずこれを外部のjsonファイルにしてみる。「data.json」を作成する。

{
    "name": "羊",
    "count": 0
}

「App.js」で「database.json」をインポートする。dataJsonという名前で。

import dataJson from './data.json';

変数datadataJsonを読み込み

const data = dataJson;

ローカルでReactアプリを起動

$ npx start

うむ、ここまでは問題ない! f:id:idr_zz:20210220050821j:plain

「数える」を押すとちゃんと羊の数が増える。 f:id:idr_zz:20210220050859j:plain

importでjsonデータを読み込めることは以前も経験済み

※参考:【React】ピュアなTypeScriptモジュールを追加してみる(Reactとメタ言語の比較-6) - クモのようにコツコツと

ここまでのソース(GitHub)

※参考:GitHub - ryo-i/react-hook-test at e6e3141ee57ddef891e8fab2c8f801a84c0bc770

JSONファイルをFetch APIで読み込んでみる(エラー!)

ReactドキュメントのAPIの解説で、Fetch APIでJSONデータを読み込む例があった。

※参考:AJAX と API – React

事例でコンテクストやフックと連携している!よし、いっちょやってみっか〜(悟空)


先ほどのJSONデータを今度はFetch APIで読み込んでみよう。

「data.json」のimportをコメントアウトする。

// import dataJson from './data.json';

ブラウザ確認。うむ、「dataJsonが見つからないよー」となる。 f:id:idr_zz:20210220051203j:plain


変数dataの宣言はletにして後から上書き可能にする。中身は空の配列。

let data = {};

変数getDataでFetch APIを実行

const getData = () => {
  fetch('./data.json')
      .then(res => res.json())
      .then(
        (result) => {
          data = result;
        },
        (error) => {
          console.log(error);
          const errData = {
            name: '名無し',
            count: 0
          }
        }
      )
};
getData();
  • 変数getDataの値はアロー関数
  • fetch()を実行。引数はimportの時と同じパス./data.json
  • 一つ目のthen()、レスポンスをjson形式で返す
  • 二つ目のthen()、成功だったら結果resultdataに入れる
  • errerの場合は、エラー用の初期値(名前は「名無し」)を入れる
  • 関数getData()を実行(ページ読み込み時に実行される)

ブラウザ確認、おや?データが取れてない。。 f:id:idr_zz:20210220053009j:plain

「数える」を押してもNaN(非数)になっとる。。 f:id:idr_zz:20210220053120j:plain

※参考:NaN - JavaScript | MDN

デベロッパーツールで成功時の処理とエラー時の処理にブレークポイントを入れたところ、成功時の処理はスルーされてエラー時の処理で止まる。 f:id:idr_zz:20210220053249j:plain

どうやらfetch()にjsonファイルのパス./data.jsonを入れるだけではちゃんと読み込まれなようだ(フルパスならいけるかもだがよくわからず…)。

ただ、画面上にはエラー用に用意した「名無し」の初期値も認識されないのも不思議。。

json-serverを設定

以前、json-serverとFetch APIでCRUD操作した。これを使ってみよう。

※参考:【JS】Fetch APIを使ってJSON ServerにCRUDする - クモのようにコツコツと

その時に参考にした記事の中にReactのCreate React APIと連携した事例もあった!

※参考:【初心者向け】非同期にCRUDするReactアプリケーションを作る - Qiita


まずjson-serverをインストールする

$ npm install json-server

package.jsonを見ると、json-serverが追加された!

  "dependencies": {
    "@testing-library/jest-dom": "^5.11.9",
    "@testing-library/react": "^11.2.5",
    "@testing-library/user-event": "^12.7.1",
    "gh-pages": "^3.1.0",
    "json-server": "^0.16.3", // 追加された!
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
    "react-scripts": "4.0.2",
    "web-vitals": "^1.1.0"
  },

json-serverを起動するスクリプトを追加する

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "predeploy": "npm run build",
    "deploy": "gh-pages main -d build",
    "json-server": "json-server --port 3001 --watch db.json" // 追加
  },
  • json-serverを起動し、port番号3001を開き、watchdb.jsonを監視する

Create React Appのport番号が3000なので重複しないように3001にした。


お、フォルダ直下に「db.json」が作成された! f:id:idr_zz:20210220054103j:plain

この中に下記の内容を入れる

{
  "data": [
    {
      "name": "羊",
      "count": 0,
      "id": 1
    }
  ]
}

json-serverを起動!(Create React App実行中のため別のターミナルを立ち上げる)

$ npm run json-server

下記のURL(port番号は3001)でdataの一つ目を開いてみると…

http://localhost:3001/data/1/

おお!データが表示された! f:id:idr_zz:20210220054209j:plain

よし、このURLにFecth APIで接続する。

json-serverをFetch APIで読み込む(タイミングが合わない!)

変数dataconsole.log()追加(dataの状態を調べたいため)

let data = {};
console.log(data);

先ほどgetDatafetch()のURLを変更、console.log()も追加

const getData = () => {
  fetch('http://localhost:3001/data/1/')
      .then(res => res.json())
      .then(
        (result) => {
          data = result;
          console.log(data);
        },
        (error) => {
          console.log(error);
          const errData = {
            name: '名無し',
            count: 0
          }
          data = errData;
        }
      )
};
getData();

Appコンポーネントの中にもconsole.log()追加

function App() {
  console.log(data);
  return (
    <div className="App">
      <header className="App-header">
        <!-- 中略 -->
        <Context.Provider value={data} >
          <Count />
        </Context.Provider>
      </header>
    </div>
  );
}

先ほど画面にdataの内容が表示されないのが不思議だったため、console.log()で調査してみる。


ブラウザ挙動確認

ありゃ?Fetch APIの値がやはり表示されないっ! f:id:idr_zz:20210220054834j:plain

数えても非数(NaN)のまま。 f:id:idr_zz:20210220054905j:plain

コンソールを見ると、おお、Fetch APIでjson-serverがちゃんと読み込まれている! f:id:idr_zz:20210220055018j:plain しかし最後のタイミングで実行されているな…(Appコンポーネントは初期値の空欄が読み込まれている)


試しにdataApp()の中でも上書きする(名前を「山羊」、数字をnullにした)

function App() {
  data = {
    name: '山羊',
    count: null
  };
  console.log(data);
  return (
    <div className="App">
      <header className="App-header">
        <!-- 中略 -->
        <Context.Provider value={data} >
          <Count />
        </Context.Provider>
      </header>
    </div>
  );
}

ブラウザ確認。おお、今後は「山羊」になった!数字はnullなので空欄。 f:id:idr_zz:20210220055143j:plain

山羊を数えることはできるようになった♪ f:id:idr_zz:20210220055347j:plain

コンソールを見ると、やはりFetch API(羊)は最後に実行されているな。 f:id:idr_zz:20210220055826j:plain

なるほど、Appコンポーネントのレンダリングより後にFetch APIが実行されるからjson-serverの値が表示されないのか。


ここまでのソース(GitHub)

※参考:GitHub - ryo-i/react-hook-test at c07466d4a61e212d31da0c96a10398ec210f6e3f

Reactライフサイクルと非同期通信

Fetch APIが最後に実行されるのは非同期通信だから。非同期通信は「この処理を行なっている間に別の処理も実行できる(同時進行)」的な仕組みで、もしこの処理の中にエラーが起こっても別の処理に影響が出ないメリットがある。

※参考:非同期処理を理解する - Sansan Builders Blog

しかし、今回の場合は非同期通信によって実行して欲しいタイミングで実行されず、先にReactのレンダリングが実行されている。


ReactのAJAXの解説を見直すと

コンポーネントのどのライフサイクルで AJAX コールすべきか?

AJAX コールによるデータ取得は componentDidMount のライフサイクルメソッドで行うべきです。データ取得後に setState でコンポーネントを更新できるようにするためです。

※参考:AJAX と API – React

むむ、まさにこのReactの「ライフサイクル」と合わないタイミングで実行しているということか。


「componentDidMount」とは何か

componentDidMount() は、コンポーネントがマウントされた(ツリーに挿入された)直後に呼び出されます。DOM ノードを必要とする初期化はここで行われるべきです。リモートエンドポイントからデータをロードする必要がある場合、これはネットワークリクエストを送信するのに適した場所です。

※参考:React.Component – React

マウントとは?

マウント コンポーネントのインスタンスが作成されて DOM に挿入されるときに、これらのメソッドが次の順序で呼び出されます。

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

※参考:React.Component – React

こちらの「ライフサイクル図」のような流れになるらしい

f:id:idr_zz:20210220060059j:plain

※参考:React lifecycle methods diagram


これを踏まえて先ほどのAPIの解説を見直すと、クラスコンポーネントと関数コンポーネントでFetch APIが下記のタイミングで実行されている。

  • クラスコンポーネントのcomponentDidMount()setState()
  • 関数コンポーネントのフック(ReactHooks)でuseEffect()

※参考:AJAX と API – React


フックのuseEffect()componentDidMount()と同義?

React のライフサイクルに馴染みがある場合は、useEffect フックを componentDidMount と componentDidUpdate と componentWillUnmount がまとまったものだと考えることができます。

※参考:副作用フックの利用法 – React

同義どころかさらに広い範囲をカバーしているようだ。

フックのuseEffect()でFetch APIを実行してみる

ではReactドキュメントのフックの事例を参考にuseEffect()でFetch APIを実行してみる。

※参考:AJAX と API – React

AppコンポーネントのgetData()はコメントアウトする

/* const getData = () => {
  fetch('http://localhost:3001/data/1/')
      .then(res => res.json())
      .then(
        (result) => {
          data = result;
          console.log(data);
        },
        (error) => {
          console.log(error);
          const errData = {
            name: '名無し',
            count: 0
          }
          data = errData;
        }
      )
};
getData(); */

Countコンポーネント、前回namecontextのプロパティを取得しただけだったが

    const context = useContext(Context);
    const name = context.name; // ここ
    const [count, setCount] = useState(context.count);

※参考:【React】フック(React Hooks)事始め:useState、useEffect、useContext - クモのようにコツコツと

nameuseStateにする

    const context = useContext(Context);
    const [name, setName] = useState(context.name); // 変更
    const [count, setCount] = useState(context.count);

useEffect()でFetch APIを設定

    useEffect(() => {
        fetch('http://localhost:3001/data/1/')
            .then(res => res.json())
            .then(
                (result) => {
                    console.log(result);
                    setName(result.name);
                    setCount(result.count);
                },
                (error) => {
                    console.log(error);
                    const errData = {
                        name: '名無し',
                        count: 0
                    }
                    setName(errData.name);
                    setCount(errData.count);
                }
            );
    }, []);
  • useEffect()の中でfetch()を実行(先ほどのgetData()の内容がベース)
  • 2つ目のthen()setName()result.nameを取得、setCount()result.countを取得
  • error時の処理、setName()errData.nameを取得、setCount()errData.countを取得
  • 最後、useEffect()の第二引数に空の配列[]を入れる

useEffect()の最後(第二引数)の空の配列[]を入れている。これは何か。

もしも副作用とそのクリーンアップを 1 度だけ(マウント時とアンマウント時にのみ)実行したいという場合、空の配列 ([]) を第 2 引数として渡すことができます。

※参考:副作用フックの利用法 – React

一回だけ実行したい時に付ける。前回設定したtitleタグ設定のuseEffect()は更新のたびに反映したかったので別にした。


ブラウザ確認、おお!ついに名前が「羊」になった!! f:id:idr_zz:20210220061531j:plain

羊を数えられてたどーー♪ f:id:idr_zz:20210220061613j:plain

コンソールを見ると最初にAppコンポーネントから読み込んだコンテクストの初期値「山羊」が実行されているが、その後にFetch APIで読み込んだdb.jsonの「羊」が実行されている! f:id:idr_zz:20210220061715j:plain

Fetch APIのURLをjson-serverをクラウド上のURLに変更

FetchのURLがlocalhostのままだとGitHub Pagesではエラーになった(fetchエラーの設定値「名無し」が表示される)

fetch('http://localhost:3001/data/1/')

json-serverはGitHubと連携しているのでコミットするとクラウド上のURLが作られる。

※参考:Node の JSON Server とそのサービス My JSON Server が凄く便利だった | エクセルソフト ブログ

ここだ!この「my-json-server」のURLに変更しよう。

※参考:https://my-json-server.typicode.com/ryo-i/react-hook-test/data/1

fetchのURLを変更

fetch('https://my-json-server.typicode.com/ryo-i/react-hook-test/data/1')

GitHub Pages上でもちゃんと表示された!


ここまでのソース(GitHub)

※参考:GitHub - ryo-i/react-hook-test at 720f3fd78d3de81be32636be2a800d91962087d5

プレビュー(GitHub Pages)

※参考:React App

最後に

ということでFetch APIを使ってReactフックでjson-serverの値を読み込むことができましたー。

これまでReactの中だけで処理を実行していた時にはあまり認識していなかった「Reactライフサイクル」。今回、外部のデータベースを読み込むことで実行タイミングのシビアさを体験し、Reactライフサイクルの理解が深まりましたー。

次は、Firebaseのデータベースの値を読み込んでみたいと思います。Firebaseこれまでいろいろ触って来ましたがReact環境で一緒に使ったことはなかったので楽しみです。(json-serverはクラウド上ではReadオンリーのため)

それではまた!


※参考:Reactを習得するためにやったことまとめ
qiita.com