【React Hooks】useEffectで無限ループを解決する

「React Hooks で、1000ミリ秒間隔でランダムな数字を10回描写する」のが目標です。

前回の[【async/await版】JavaScriptでループ中にスリープしたい。それも読みやすいコードで](https://qiita.com/wafuwafu13/items/2b8eecdba41d7b98b51e)という記事で、「1000ミリ秒間隔でランダムな数字を出力する」ことはできたので今回は、「React Hooks で描写する」ことに挑戦しました。
その途中で無限ループに陥ってしまったので、失敗例と成功例をメモしておこうと思います。


## 失敗例1　useEffectを使わず無限ループに陥る

useEffectを使わず main関数を直で呼び出しているコードです。

```javascript

import React, { useState } from 'react';
import { StyleSheet, View, Text } from 'react-native';

const StartScreen = () => {
    const [count, setCount] = useState(0);

    function sleep(milliseconds: number) {
        return new Promise((resolve) => setTimeout(resolve, milliseconds));
      }
      
    async function main() {
      for (let i = 0; i < 10; i++) {
          setCount(Math.random())
          await sleep(1000);
      }
    }

    main()
    
    return(
        <View>
            <Text>{count}</Text>
        </View>
    )
}
```

このコードでは無限ループが起こってしまい、``[Unhandled promise rejection: Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop.]`` というエラーが出てしまいます。

これは、関数型の React コンポーネントにおいて、**内部状態またはプロパティが変更されると、コンポーネントの関数が再実行される**からです。
そのため、

1  ``main()``実行 
2  ``{count}``変更 
1  ``main()``実行 
2  ``{count}``変更
:
:

という無限ループが生じているのです。


## 失敗例2　useEffectの第二引数になにも設定せず無限ループに陥る

useEffect内でmain関数を実行し、useEffectの第二引数にはなにも設定していないコードです。

```javascript
import React, { useState, useEffect } from 'react';
import { StyleSheet, View, Text } from 'react-native';

const StartScreen = () => {
    console.log('StartScreen')
    const [count, setCount] = useState(0);

    function sleep(milliseconds: number) {
        return new Promise((resolve) => setTimeout(resolve, milliseconds));
      }
      
    async function main() {
      console.log('main')
      for (let i = 0; i < 10; i++) {
          setCount(Math.random())
          await sleep(1000);
      }
    }
      
    useEffect(() => {
      console.log('useEffect')
      main()
    })
    
    return(
        <View>
            <Text>{count}</Text>
        </View>
    )
}
```

このコードでも無限ループが生じてしまいます。

これは、関数型の React コンポーネントにおいて、**関数の結果が前回の呼び出し時と異なれば、レンダリングが発生する**からです。

コンソールの結果を見てみると、以下のような無限ループが生じています。

```
StartScreen
useEffect
main
StartScreen
useEffect
main
:
:
```

これは、

1  ``StartScreen``実行
2  コンポーネントが DOM にレンダリングされる
3  ``useEffect``が呼ばれる
4  ``main()``実行で``{count}``変更
5  もう一度``StartScreen``実行
6  コンポーネントが DOM にレンダリングされる。
7  ``useEffect``が呼ばれる
8  ``main()``実行で``{count}``変更
9  さらに``StartScreen``実行
10  前回の実行時と返却されるJSXの値(count)が異なるため、コンポーネントが DOM にレンダリングされる
11 = 3 ``useEffect``が呼ばれる
12 = 4 ``main()``実行で``{count}``変更
:
:

という無限ループが生じているためです。


## 成功例　useEffectの第二引数に空配列を指定して無限ループから脱出

失敗例2のコードに空配列を足しただけのコードです。

```javascript

import React, { useState, useEffect } from 'react';
import { StyleSheet, View, Text } from 'react-native';

const StartScreen = () => {
    const [count, setCount] = useState(0);

    console.log('StartScreen')

    function sleep(milliseconds: number) {
        return new Promise((resolve) => setTimeout(resolve, milliseconds));
    }
      
    async function main() {
      console.log('main')
      for (let i = 0; i < 5; i++) {
          console.log('main loop')
          setCount(Math.random())
          await sleep(1000);
      }
    }
      
    useEffect(() => {
      console.log('useEffect')
      main()
    },[])
    
    return(
        <View>
            <Text>{count}</Text>
        </View>
    )
}
```

このコードでは、ランダムな数字が1000ミリ秒間隔で5回表示されます。

これは、**useEffectの第二引数に[]を指定すると、マウント時のみ第１引数の関数が実行される**からです。

コンソールの結果は、以下のようになっています。

```
StartScreen
useEffect
main
main loop
StartScreen
main loop
StartScreen
main loop
StartScreen
main loop
StartScreen
main loop
StartScreen
````

これは、

1  ``StartScreen``実行
2  コンポーネントが DOM にレンダリングされる
3  ``useEffect``が呼ばれる
4  ``main()``実行
5  ``{count}``が変更される
6  ``StartScreen``実行
7  コンポーネントが DOM にレンダリングされる
5 ``{count}``が変更される
6  ``StartScreen``実行
7  コンポーネントが DOM にレンダリングされる
:
:
5 ``{count}``が変更される
6  ``StartScreen``実行
7  コンポーネントが DOM にレンダリングされる

という処理が実行されていることを意味します。


ちなみに、useEffectの第二引数に``[count]``を設定すると、**useEffectがcount変更時に呼び出されるため無限ループが生じます。**


#### 参考記事

[関数型Reactコンポーネントでレンダリングと副作用Hookが実行されるタイミング
](https://www.hypertextcandy.com/when-hook-is-called)
[useEffect完全ガイド](https://overreacted.io/ja/a-complete-guide-to-useeffect/)

useEffect完全ガイドはまだあまり読めていません。
useEffectの第二引数に[]を指定すると、マウント時のみ第１引数の関数が実行されるのがまだあまり腑に落ちていないのでじっくり読んでいこうと思います。