8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-08-13

掌田津耶乃 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

一部省略します

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となるもの、なので下記のobj1obj2のように形だけ同じでも別のオブジェクトなら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の挙動とどうやって無限ループを防ぐかについて記事にまとめてみました!ぜひこちらも参照してください

React Hooksを理解し、無限ループを防ぐ

余談

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

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

感想

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

8
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?