掌田津耶乃 React.js&Next.js超入門 第2版で学習を進めていていますが、Chapter5の「fetch APIでJSONデータにアクセスする」のサンプルコードで無限ループが発生しブラウザがフリーズしたので、その点を修正するコードを掲載します。
また、この本にはステートの更新とコンポーネントがいつ再実行されるかについての記述が不足していると思われるので、その点についても補足しています。
またこの本には他にもバグや注意すべき点があるので次を参照してください。
React.js&Next.js超入門 第2版 サンプルコードのバグまとめ
無限ループ発生個所
関数コンポーネントHome()
内でJSONデータを取得するためにfetch(url)
を実行しています。この取得が成功した場合に呼ばれるres=>setData(res)
でdataステートにオブジェクトを設定しているのですが、ステートにオブジェクトを設定した途端、レンダリングのためもう一度この関数コンポーネントが呼ばれ、再びfetch(url)
が呼ばれという無限ループに陥ってしまいます。(詳しい理由は後述します。)
修正前のindex.js
一部省略します
import Layout from '../components/layout';
import { useEffect, useState } from 'react';
export default function Home() {
const url='./data.json';
const [data,setData]=useState({message:'',data:[]});
fetch(url)
.then(res=>res.json())
.then(res=>setData(res))//ここでステートが更新され、無限ループ発生
return (
<div>
<Layout header="Next.js" title="Top page.">
//...略...
</Layout>
</div>
)
}
修正案
下のコードのように、fetch(url)...
の部分を副作用フック(useEffect
)にコールバック関数として設定し、さらに、useEffect
の第二引数に[]
を指定します。こうすることで、fetch(url)...
はこの関数コンポーネントが最初実行されたとき(マウント時)だけに実行されるようになり、無限ループは解消します。
import Layout from '../components/layout';
import { useEffect, useState } from 'react';
export default function Home() {
const url='./data.json';
const [data,setData]=useState({message:'',data:[]});
//fetchをuseEffectに渡す
useEffect(()=>{
fetch(url)
.then(res=>res.json())
.then(res=>setData(res))
},[])
return (
<div>
<Layout header="Next.js" title="Top page.">
//...略...
</Layout>
</div>
)
}
何が問題か
まず、前提知識として、ステートが更新されると表示も更新されます。つまり関数コンポーネントが再実行されます。また、何をもって「ステートが更新された」とするかというと、それは前回のステートの値とObject.is
で比較してfalse
の場合、「ステートが更新された」と判定されます。
Object.is
で比較してfalse
となるもの、なので下記のobj1
、obj2
のように形だけ同じでも別のオブジェクトならfalse
になります。
const obj1 = { foo: 'bar' };
const obj2 = { foo: 'bar' };
つまりステートにobj1
が入っている状態で、obj2
が設定されると関数コンポーネントが再実行されます。
今回、setData(res)
で設定されるres
は受信したデータをJSONでパースしたオブジェクトなので、当然、データの受信ごとに別のオブジェクトとなり、再レンダリングが発生し、再びfetch(url)...
が実行され、と無限ループが発生してしまいます。
どう解決したか
副作用フック(useEffect
)を使いました。
useEffect
に渡した関数はステートが更新されたとき(正確には前回のステートの値とObject.is
で比較してfalse
だったとき)に実行されるのに加え、実は関数コンポーネントのマウント時、つまり初回にかならず実行されます。そして、useEffect
の第二引数に空の配列[]
を渡すことで、このマウント時のみに関数が実行されるようにできます。
(How the useEffect Hook Works (with Examples)参照)
こうすることで、fetch(url)...
はこの関数コンポーネントが最初実行されたとき(マウント時)だけに実行されるようになり、無限ループは解消されます。
以上React Hooksの挙動とどうやって無限ループを防ぐかについて記事にまとめてみました!ぜひこちらも参照してください
余談
以上のことが分かっていると、この本の「4-3副作用フックの利用」の240p(副作用のスキップ)でのuseEffect
が無限ループになる理由を「JSXでレンダリングしてるから」と説明しているのも不正確であることがわかります。
JSX構文も一種のオブジェクトと解釈されます。そしてステートに前回と同じ形のJSXを代入しても、Object.is
で比較してfalse
と判定されてしまうので、ステートが更新されたと判定され、useEffect
が再度呼ばれてしまうので無限ループが発生してしまうのです。
感想
とても分かりやすい本だと思いますが、ステートの更新とコンポーネントがいつ再実行されるかは重要だと思われるのでここら辺の話も次の版ではぜひ盛り込んでほしいと思います。