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>
}