「React Hooks で、1000ミリ秒間隔でランダムな数字を10回描写する」のが目標です。
前回の【async/await版】JavaScriptでループ中にスリープしたい。それも読みやすいコードでという記事で、「1000ミリ秒間隔でランダムな数字を出力する」ことはできたので今回は、「React Hooks で描写する」ことに挑戦しました。
その途中で無限ループに陥ってしまったので、失敗例と成功例をメモしておこうと思います。
失敗例1 useEffectを使わず無限ループに陥る
useEffectを使わず main関数を直で呼び出しているコードです。
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の第二引数にはなにも設定していないコードです。
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のコードに空配列を足しただけのコードです。
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の第二引数に[]を指定すると、マウント時のみ第1引数の関数が実行されるからです。
コンソールの結果は、以下のようになっています。
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が実行されるタイミング
useEffect完全ガイド
useEffect完全ガイドはまだあまり読めていません。
useEffectの第二引数に[]を指定すると、マウント時のみ第1引数の関数が実行されるのがまだあまり腑に落ちていないのでじっくり読んでいこうと思います。