stateが更新されない
ReactでsetInterval()を使ってstateを更新しようとするとうまく更新されない。
import React, { useEffect, useState } from 'react'
export const Sample = () => {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
console.log('ticking')
setCount(count + 1)
}, 1000)
return () => clearInterval(timer)
}, [])
return <div>count is: {count}</div>
}
useEffect()の外に書き出してみても変わりはない。
import React, { useEffect, useState } from 'react'
export const Sample = () => {
let timer
const [count, setCount] = useState(0)
const updateCount = () => {
timer = !timer && setInterval(() => {
console.log('ticking')
setCount(count + 1)
}, 1000)
}
useEffect(() => {
updateCount()
return () => clearInterval(timer)
}, [])
return <div>count is: {count}</div>
}原因はsetInterval()に渡したfunctionは渡したときの状態のままなので、その中の変数も変化しないこと。
つまり、
() => {
console.log('ticking')
setCount(count + 1)
}というfunctionを渡してsetInterval()した時、この時点でのcountは0なので、このfunctionが何回実行されても新しいcountは1にしかならない。
Functional updates
実はReactのsetState()にはfunctionを渡すことができ、その時に前回のstateの値を引数として受け取ることができる。
これを使うことで毎回新しいstateの値を元に更新することができる。
import React, { useEffect, useState } from 'react'
export const Sample = () => {
let timer
const [count, setCount] = useState(0)
const updateCount = () => {
timer = !timer && setInterval(() => {
console.log('ticking')
+ setCount(prevCount => prevCount + 1) // new
}, 1000)
}
useEffect(() => {
updateCount()
return () => clearInterval(timer)
}, [])
return <div>count is: {count}</div>
}
無事countが更新されるようになった!
setIntervalの中でstateを使う
まだ少し問題がある。
例えばcountが3になったときに何かの処理をして、stateの更新を止めたいとする。
以下のようにif文を足してみるが、これでは止まってくれない。
import React, { useEffect, useState } from 'react'
export const Sample = () => {
let timer
const [count, setCount] = useState(0)
const updateCount = () => {
timer = !timer && setInterval(() => {
console.log('ticking')
setCount(prevCount => prevCount + 1)
}, 1000)
+ if (count === 3) { // new
+ console.log('stop!')
+ clearInterval(timer)
+ }
}
useEffect(() => {
updateCount()
return () => clearInterval(timer)
}, [])
return <div>count is: {count}</div>
}
ここでの原因はupdateCount()が一度しか実行されていないこと。当然count === 3というif文も1回しか考慮されていない。
次のようにsetCountの中に記述すれば問題ないが、setStateの中が大きくなっていくのは気持ちが悪い。
setCount(prevCount => {
prevCount + 1
if (prevCount+1 === 3) {
// do something
}
})この問題はuseEffect()の第2引数にcountを追加すれば解決する。
これでcountの値が更新されるたびuseEffect()が実行されるので、updateCount()の中のcountは常に新しい値になる。
import React, { useEffect, useState } from 'react'
export const Sample = () => {
let timer
const [count, setCount] = useState(0)
const updateCount = () => {
timer = !timer && setInterval(() => {
console.log('ticking')
setCount(prevCount => prevCount + 1)
}, 1000)
if (count === 3) {
console.log('stop!')
clearInterval(timer)
}
}
useEffect(() => {
updateCount()
return () => clearInterval(timer)
+ }, [count]) // new
return <div>count is: {count}</div>
}
ということはつまり、今ならfunctional updatesを使わずとも
setCount(count + 1)こう書いてstateを更新することができる。
ただ、その方法でいくと使いたいstateが増えるたびにuseEffect()がゴチャゴチャしてくるので、stateを更新するだけならfunctional updatesを使うに越したことはないと思う。
Final code
import React, { useEffect, useState } from 'react'
export const Sample = () => {
let timer
const [count, setCount] = useState(0)
const updateCount = () => {
timer = !timer && setInterval(() => {
setCount(prevCount => prevCount + 1)
}, 1000)
if (count === 3) clearInterval(timer)
}
useEffect(() => {
updateCount()
return () => clearInterval(timer)
}, [count])
return <div>count is: {count}</div>
}


