発生した問題
Reactに触り始めのころ、実際にしたことです。
画面を開いたときに動く、1秒ずつ進むタイマーを作ろうとして、以下のようなコードを書いたのですが、実際にはタイマーが2秒ずつに進んでしまう、ということがありました。
App.tsx
import { useState, useEffect } from 'react'
function App() {
const [time, setTime] = useState(0);
/**
* 最初の処理
*/
useEffect(() => {
// 1秒ごとに1ずつ足していくインターバル
setInterval(() => {
setTime((prevTime) => prevTime + 1);
}, 1000);
}, []);
return (
<>
<div>{time}</div>
</>
)
}
export default App
解決法1
この原因としては、React の StrictMode により、マウント時にuseEffectの処理が2回行われ、タイマーが2回起動していたからです。
StrictModeについての参考:https://qiita.com/asahina820/items/665c55594cfd55e6f14a
この問題は useEffect の return にタイマーの終了処理を書くことで解決します。(クリーンアップ関数)
App.tsx
import { useState, useEffect } from 'react'
function App() {
const [time, setTime] = useState(0);
/**
* 最初の処理
*/
useEffect(() => {
// 1秒ごとに1ずつ足していくインターバル
const timerId =setInterval(() => {
setTime((prevTime) => prevTime + 1);
}, 1000);
// タイマーのクリーン
return () => {
clearInterval(timerId);
};
}, []);
return (
<>
<div>{time}</div>
</>
)
}
export default App
こうすることで、StrictMode による2回目のマウント時には、前のタイマーはクリーンされ停止し、1秒ずつの動作になります。
解決法2
またはタイマー用のstateを追加し、それを依存配列に持つuseEffect内でタイマー処理を行うことでも、正常に動作します。
App.tsx
import { useState, useEffect } from 'react'
function App() {
const [time, setTime] = useState(0);
// タイマー用state
const [isTimerStop, setIsTimerStop] = useState(true);
/**
* 最初の処理
*/
useEffect(() => {
// タイマー用stateの値を変更
setIsTimerStop(false);
}, []);
/**
* タイマー処理
*/
useEffect(() => {
if (isTimerStop) {
// タイマーが止まっている場合
return;
}
// 1秒ごとに1ずつ足していくインターバル
const timerId = setInterval(() => {
setTime((prevTime) => prevTime + 1);
}, 1000);
// タイマーのクリーン
//(なくても1秒ずつの動作にはなるが、タイマーを一時停止させる必要がある場合は必須)
return () => {
clearInterval(timerId);
};
}, [isTimerStop]);// 依存配列にタイマー用stateを持たせる
return (
<>
<div>{time}</div>
</>
)
}
export default App
このように書いた場合は、StrictModeにより setIsTimerStop() は2回動作しますが、useEffectでは依存配列の値が同じ場合は処理が動かないので、タイマーが2回起動することはなくなります。
以上です。