Breathnote
Reactではstateの更新が即時反映されない場合がある

Reactではstateの更新が即時反映されない場合がある

参考記事は以下の通り。

なにが問題か

下の例では、ブラウザとコンソールともに1と表示されることを期待しています。しかし、実際にコンポーネントをレンダリングすると、ブラウザに1、コンソールに0と表示されてしまいます。

Component.tsx
import { useEffect, useState } from 'react'

function Component() {
  const [count, setCount] = useState<number>(0)

  useEffect(() => {
    setCount(count + 1) // stateをインクリメント
    console.log(count) // 更新後のstateをコンソールに表示したい
  }, [])

  return <>{count}</>
}

export default Component

対処法

コードに以下の修正を加えることで、期待通りの動作を実現できます。

Component.tsx
import { useEffect, useState } from 'react'

function Component() {
  const [count, setCount] = useState<number>(0)

  useEffect(() => {
    setCount(count + 1) // stateをインクリメント
  }, [])

  useEffect(() => {
    console.log(count) // 更新後のstateをコンソールに表示したい
  }, [count])

  return <>{count}</>
}

export default Component

useEffectの第二引数にstateを指定することにより、stateの更新後にconsole.logを実行できるようになります。

「最初のコードでも、state更新後にログを表示していたのでは?」と思うかもしれません。

私は思いました。

なぜこの問題が起こるのか

それは、Reactがstateの更新処理を非同期で行うからです。

Reactは、stateの変更を検知すると、そのstateに関連するコンポーネントを再レンダリングします。

この仕様において、同期的にstateを更新すると、stateの数に応じて大量のレンダリングが走ってしまいます。Reactではこれを抑制するために、stateの変更をある程度の単位にまとめ、一括でバッチ処理しています。

今回の例で言えば、setCountの段階ではまだstateが更新されていないということです。

なぜ上記の対処法で解決するのか

useEffect内の処理は、コンポーネントの再レンダリング後に実行されるからです。

先ほど「setCountの段階では、stateは更新されない」と述べましたが、実際にstateが更新されるのは、次にuseStateが呼ばれた時です。useStateが呼ばれるのは、stateの更新に伴ってコンポーネントが再レンダリングされた時。その後にuseEffect内の処理を実行することで、更新後の値でconsole.logが実行できるのです。