株式会社ココロファン - エンジニアリング事業部所属の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初級者なので、こういった嵌ったことがあれば記事にしてきたいと思います。