株式会社ココロファン - エンジニアリング事業部所属のhazamaです。
サイト制作・運用業務をメインに行っています。
Reactでタイマーアプリを作っている時にuseStateとsetIntervalを使用したところ、
stateが更新されない事象に嵌りました。
その時のコード
import React, { useEffect, useState } from 'react'
export const Timer = () => {
const [time, setTime] = useState(60)
useEffect(() => {
const count = setInterval(() => {
setTime(time - 1)
}, 1000)
return () => clearInterval(count)
}, [])
return <div>あと {time}秒</div>
}
特に真新しいものはありませんが、setIntervalメソッドを使用して1秒ずつtimeを減らすだけの
単純なコードですが、こちらを実行するとタイマーは59秒から変わりません。
setIntervalがちゃんと動いていないのかと思いましたが、console.logを仕込んだら1秒ずつ出力されている。
つまりsetIntervalは動いているけど、stateの値が更新されていないということがわかりました。
原因
どうやらクロージャーの問題のようです。
setIntervalのコールバック関数がクロージャーを作成し、その中でuseStateの値を参照している場合、setIntervalが
実行された時点でのuseStateの値が固定されてしまうようです。
なるほど、先程のコードの場合timeの初期値は60になっているので、setTimeに渡されるのは59になってしまうと・・・。
解消を試みる
原因がわかったので、useStateの値を直接参照するのはやめprveState(以下ではprevTimeと命名)で
更新される前の値を参照し減算するようにしました。
import React, { useEffect, useState } from 'react'
export const Timer = () => {
const [time, setTime] = useState(60)
useEffect(() => {
const count = setInterval(() => {
setTime(prevTime => prevTime - 1)
}, 1000)
return () => clearInterval(count)
}, [])
return <div>あと {time}秒</div>
}
結果はちゃんと動くようになりました!!!が、そもそも基本的な見落としがあります。
useEffect内でtimeを参照しているにもかかわらず、第二引数にて何も指定していません。
第二引数の依存配列にtimeを参照するようにすれば動くようになります。普通にこっちの方がシンプルでいいですね。
import React, { useEffect, useState } from 'react'
export const Timer = () => {
const [time, setTime] = useState(60)
useEffect(() => {
const count = setInterval(() => {
setTime(time - 1)
}, 1000)
return () => clearInterval(count)
}, [time])
return <div>あと {time}秒</div>
}
これで解決と思っていたら、マイナス秒に突入していきました。。。
今度はclearIntervalが動いていない。
setInterval によるタイマーの更新処理が非同期で実行されているらしく、値が即座に反映されないことから
今回のようにマイナス秒に突入してしまうことがあるみたいです。
解消を試みる(2回目)
調査したところ、setTimeの引数として新しい値を計算する関数を渡すると常に最新の値を基として更新が行われるようです。
以下のように修正してみました。結果はマイナス秒に突入するこもなくなく、無事解消できました!
import React, { useEffect, useState } from 'react';
export const Timer = () => {
const [time, setTime] = useState(60);
useEffect(() => {
const intervalId = setInterval(() =>
time === 0 ? clearInterval(intervalId) : setTime(time - 1), 1000);
return () => {
clearInterval(intervalId);
};
}, [time]);
return <div>あと {time}秒</div>;
};
最後に
コードの量としては大したことないのですが、ここに至るまでに大分時間がかかりました。
1つ知見を得られたのは良かったのですが、それにしても嵌りました。
まだまだReact初級者なので、こういった嵌ったことがあれば記事にしてきたいと思います。