[React] setInterval()の中でsetState()を使う

May 20, 2021

programming
article cover

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

screen shot

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の値を引数として受け取ることができる。

screen shot

これを使うことで毎回新しい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>
}

screen shot

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

screen shot

ここでの原因は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>
}

screen shot

ということはつまり、今なら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>
}

Profile picture

元カメラマン。今はベルリンで働くWEBエンジニア。
スロバキア人の妻とドイツ猫二匹の多国籍家族。