Reactの続きです。前回はフック(React Hooks)を事始めました。基本のフックuseState、useEffect、useContextを体験しました。今回はこのフックにFetch APIを使ってjson-serverのデータベースを連携してみました。最初、処理実行のタイミングがうまくいかず、試行錯誤の過程でReactのライフサイクルについて理解が深まりました。それではいきましょう!
【目次】
- 前回のおさらい
- importでJSONファイルを読み込む
- JSONファイルをFetch APIで読み込んでみる(エラー!)
- json-serverを設定
- json-serverをFetch APIで読み込む(タイミングが合わない!)
- Reactライフサイクルと非同期通信
- フックのuseEffect()でFetch APIを実行してみる
- Fetch APIのURLをjson-serverをクラウド上のURLに変更
- 最後に
※参考:前回記事
【React】フック(React Hooks)事始め:useState、useEffect、useContext - クモのようにコツコツと
※参考:Reactを習得するためにやったことまとめ
qiita.com
前回のおさらい
フック(React Hooks)を事始める。基本のフックuseState、useEffect、useContextの理解。
※参考:【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';
変数data
でdataJson
を読み込み
const data = dataJson;
ローカルでReactアプリを起動
$ npx start
うむ、ここまでは問題ない!
「数える」を押すとちゃんと羊の数が増える。
importでjsonデータを読み込めることは以前も経験済み
※参考:【React】ピュアなTypeScriptモジュールを追加してみる(Reactとメタ言語の比較-6) - クモのようにコツコツと
ここまでのソース(GitHub)
※参考:GitHub - ryo-i/react-hook-test at e6e3141ee57ddef891e8fab2c8f801a84c0bc770
JSONファイルをFetch APIで読み込んでみる(エラー!)
ReactドキュメントのAPIの解説で、Fetch APIでJSONデータを読み込む例があった。
事例でコンテクストやフックと連携している!よし、いっちょやってみっか〜(悟空)
先ほどのJSONデータを今度はFetch APIで読み込んでみよう。
「data.json」のimport
をコメントアウトする。
// import dataJson from './data.json';
ブラウザ確認。うむ、「dataJsonが見つからないよー」となる。
変数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()
、成功だったら結果result
をdata
に入れる errer
の場合は、エラー用の初期値(名前は「名無し」)を入れる- 関数
getData()
を実行(ページ読み込み時に実行される)
ブラウザ確認、おや?データが取れてない。。
「数える」を押してもNaN(非数)になっとる。。
デベロッパーツールで成功時の処理とエラー時の処理にブレークポイントを入れたところ、成功時の処理はスルーされてエラー時の処理で止まる。
どうやら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を開き、
watch
でdb.json
を監視する
Create React Appのport番号が3000なので重複しないように3001にした。
お、フォルダ直下に「db.json」が作成された!
この中に下記の内容を入れる
{ "data": [ { "name": "羊", "count": 0, "id": 1 } ] }
json-serverを起動!(Create React App実行中のため別のターミナルを立ち上げる)
$ npm run json-server
下記のURL(port番号は3001)でdata
の一つ目を開いてみると…
http://localhost:3001/data/1/
おお!データが表示された!
よし、このURLにFecth APIで接続する。
json-serverをFetch APIで読み込む(タイミングが合わない!)
変数data
にconsole.log()
追加(data
の状態を調べたいため)
let data = {}; console.log(data);
先ほどgetData
のfetch()
の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の値がやはり表示されないっ!
数えても非数(NaN)のまま。
コンソールを見ると、おお、Fetch APIでjson-serverがちゃんと読み込まれている! しかし最後のタイミングで実行されているな…(Appコンポーネントは初期値の空欄が読み込まれている)
試しにdata
をApp()
の中でも上書きする(名前を「山羊」、数字を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
なので空欄。
山羊を数えることはできるようになった♪
コンソールを見ると、やはりFetch API(羊)は最後に実行されているな。
なるほど、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 でコンポーネントを更新できるようにするためです。
むむ、まさにこのReactの「ライフサイクル」と合わないタイミングで実行しているということか。
「componentDidMount」とは何か
componentDidMount() は、コンポーネントがマウントされた(ツリーに挿入された)直後に呼び出されます。DOM ノードを必要とする初期化はここで行われるべきです。リモートエンドポイントからデータをロードする必要がある場合、これはネットワークリクエストを送信するのに適した場所です。
マウントとは?
マウント コンポーネントのインスタンスが作成されて DOM に挿入されるときに、これらのメソッドが次の順序で呼び出されます。
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
こちらの「ライフサイクル図」のような流れになるらしい
※参考:React lifecycle methods diagram
これを踏まえて先ほどのAPIの解説を見直すと、クラスコンポーネントと関数コンポーネントでFetch APIが下記のタイミングで実行されている。
- クラスコンポーネントの
componentDidMount()
でsetState()
- 関数コンポーネントのフック(ReactHooks)で
useEffect()
フックのuseEffect()
はcomponentDidMount()
と同義?
React のライフサイクルに馴染みがある場合は、useEffect フックを componentDidMount と componentDidUpdate と componentWillUnmount がまとまったものだと考えることができます。
同義どころかさらに広い範囲をカバーしているようだ。
フックのuseEffect()でFetch APIを実行してみる
ではReactドキュメントのフックの事例を参考にuseEffect()
でFetch APIを実行してみる。
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コンポーネント、前回、name
はcontext
のプロパティを取得しただけだったが
const context = useContext(Context); const name = context.name; // ここ const [count, setCount] = useState(context.count);
※参考:【React】フック(React Hooks)事始め:useState、useEffect、useContext - クモのようにコツコツと
name
もuseState
にする
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 引数として渡すことができます。
一回だけ実行したい時に付ける。前回設定したtitleタグ設定のuseEffect()
は更新のたびに反映したかったので別にした。
ブラウザ確認、おお!ついに名前が「羊」になった!!
羊を数えられてたどーー♪
コンソールを見ると最初にAppコンポーネントから読み込んだコンテクストの初期値「山羊」が実行されているが、その後にFetch APIで読み込んだdb.jsonの「羊」が実行されている!
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