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
が実行できるのです。