React.js&Next.js超入門 第2版の無限ループのバグ修正

[掌田津耶乃　React.js&Next.js超入門 第2版](https://www.shuwasystem.co.jp/book/9784798063980.html)で学習を進めていていますが、Chapter5の「fetch APIでJSONデータにアクセスする」のサンプルコードで無限ループが発生しブラウザがフリーズしたので、その点を修正するコードを掲載します。

また、この本にはステートの更新とコンポーネントがいつ再実行されるかについての記述が不足していると思われるので、その点についても補足しています。

またこの本には他にもバグや注意すべき点があるので次を参照してください。

[React.js&Next.js超入門 第2版 サンプルコードのバグまとめ](https://qiita.com/SolKul/items/c2f03652267951419a2e)


# 無限ループ発生個所

関数コンポーネント`Home()`内でJSONデータを取得するために`fetch(url)`を実行しています。この取得が成功した場合に呼ばれる`res=>setData(res)`でdataステートにオブジェクトを設定しているのですが、ステートにオブジェクトを設定した途端、レンダリングのためもう一度この関数コンポーネントが呼ばれ、再び`fetch(url)`が呼ばれという無限ループに陥ってしまいます。(詳しい理由は後述します。)

## 修正前の`index.js`

一部省略します

```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)...`はこの関数コンポーネントが最初実行されたとき(マウント時)だけに実行されるようになり、無限ループは解消します。


```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を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`になります。

```javascript
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)](https://daveceddia.com/useeffect-hook-examples/)参照)

こうすることで、`fetch(url)...`はこの関数コンポーネントが最初実行されたとき(マウント時)だけに実行されるようになり、無限ループは解消されます。

以上React Hooksの挙動とどうやって無限ループを防ぐかについて記事にまとめてみました！ぜひこちらも参照してください

[React Hooksを理解し、無限ループを防ぐ](https://qiita.com/SolKul/items/1cb6a4b9febde4a094e8)

# 余談

以上のことが分かっていると、[この本](https://www.shuwasystem.co.jp/book/9784798063980.html)の「4-3副作用フックの利用」の240p(副作用のスキップ)での`useEffect`が無限ループになる理由を「JSXでレンダリングしてるから」と説明しているのも不正確であることがわかります。

JSX構文も一種のオブジェクトと解釈されます。そしてステートに前回と同じ形のJSXを代入しても、`Object.is`で比較して`false`と判定されてしまうので、ステートが更新されたと判定され、`useEffect`が再度呼ばれてしまうので無限ループが発生してしまうのです。

# 感想

とても分かりやすい本だと思いますが、ステートの更新とコンポーネントがいつ再実行されるかは重要だと思われるのでここら辺の話も次の版ではぜひ盛り込んでほしいと思います。