Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】React Re-render Behind the scene.

2025年09月12日に「Dev.to」が公開したITニュース「React Re-render Behind the scene.」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

Reactで状態を更新しても`console.log`に古い値が出るのは、Reactが状態更新をすぐに行わず、現在の処理が終わった後に再レンダリングをスケジュールするからだ。そのため、新しい値での画面反映は少し後になる。

出典: React Re-render Behind the scene. | Dev.to公開日:

ITニュース解説

Reactは、ウェブサイトやウェブアプリケーションの見た目(ユーザーインターフェース)を構築するためのJavaScriptライブラリである。ウェブサイトやアプリでは、ユーザーがボタンをクリックしたり、文字を入力したりするたびに、画面の内容が変化することがよくある。このような変化を効率的かつ論理的に管理するために、Reactでは「コンポーネント」という部品と、「状態(state)」という仕組みが導入されている。状態とは、コンポーネントが内部で保持するデータであり、このデータが変わると、コンポーネントの見た目も変化する可能性がある。

今回の解説では、Reactアプリケーションでボタンがクリックされるたびに数字が増える、非常にシンプルなカウンターアプリのコードを例に、Reactが内部でどのように動作するのか、特に「状態の更新」とそれに伴う「リレンダリング」という重要なプロセスに焦点を当てて説明する。

提示されたコードは以下の通りである。

1import { useState } from 'react'
2
3const App = () => {
4  const [count, setCount] = useState(0);
5
6  const handleClick = () => {
7    setCount(count + 1);
8    console.log(count);
9  }
10
11  return (
12    <button onClick={handleClick}>Click Me</button>
13  )
14}
15
16export default App

このコードでは、useStateというReactのフックを使って、countという状態変数を定義している。初期値は0である。setCountcountの値を更新するための専用の関数だ。handleClickという関数は、ボタンがクリックされたときに実行されるように設定されている。この関数の中では、setCount(count + 1)によってcountの値を1増やそうとしている。その直後にconsole.log(count)を使って、現在のcountの値をウェブブラウザの開発者ツールにあるコンソールに出力している。

システムエンジニアを目指す初心者がこのコードを実行し、ボタンをクリックすると、多くの人は「コンソールには更新されたはずの新しい値が表示されるだろう」と直感的に考えるかもしれない。例えば、count0の時にボタンをクリックしたら、setCount(0 + 1)によってcount1になるはずなので、console.log(count)では1が表示されると予想するだろう。しかし、実際にこのコードを動かしてみると、コンソールにはボタンをクリックする前の、つまり古い値が表示されるという現象が起きる。具体的には、count0の時にクリックするとコンソールには0が表示され、画面上の表示は1に変わる。もう一度クリックするとコンソールには1が表示され、画面上の表示は2に変わる、といった具合だ。この挙動はなぜ起こるのだろうか。

この一見すると不思議な現象は、Reactの「リレンダリング」という重要な仕組みが深く関係している。リレンダリングとは、コンポーネントの状態が更新されたときに、Reactがそのコンポーネントの見た目を再構築し、画面に反映させるプロセスを指す。しかし、setState()関数(今回の例ではsetCount())が呼ばれたからといって、Reactがすぐに状態変数の値を更新し、コンポーネントを再描画するわけではない。Reactは内部で一連の決まった手順に従って動作する。

setState()が実行されると、Reactはすぐにコンポーネントをリレンダリングするのではなく、まず「リレンダリングをスケジュールする」という処理を行う。これは、Reactが「後でこのコンポーネントを更新する必要がある」というタスクを内部的なリストに追加するようなイメージだ。そして、現在実行中の同期的なコード、つまりJavaScriptの実行スレッドが処理している一連の命令がすべて完了するのを待つ。現在のコードブロックの中に他の処理があれば、Reactはまずそれらを最後まで実行するのだ。

今回のカウンターアプリの例で言えば、ボタンがクリックされてhandleClick関数が実行されると、まずsetCount(count + 1)が実行される。この時点で、Reactはcountの値を更新するためのタスクをスケジュールするが、まだcountという変数が参照している「現在の値」はすぐに更新されない。その直後にconsole.log(count)が実行される。このとき、handleClick関数はまだ同期的に実行されている途中なので、count変数が参照している値は、setCountが呼ばれる前の古い値のままである。そのため、コンソールには古い値が出力されるのだ。

handleClick関数の中のすべての同期コード(この場合はsetCountconsole.log)が実行され終わると、Reactはスケジューリングされたリレンダリングのプロセスを開始する。このリレンダリングのプロセスは、主に以下の3つのステップで構成される。

まず、「更新された状態(state)で仮想DOMを作成する」。Reactは、実際のウェブページ(ブラウザが描画するDOM)とは別に、「仮想DOM(Virtual DOM)」という、DOMの非常に軽量なコピーをメモリ上に保持している。状態が更新されると、Reactは新しい状態に基づいて、コンポーネントの新しい見た目を表す仮想DOMツリーをメモリ上に構築し直す。この時点ではまだ、ブラウザに表示されている実際の画面は何も変わっていない。

次に、「新しい仮想DOMと前の仮想DOMを比較する(差分検出、diffing)」。Reactは、新しく作成した仮想DOMツリーと、リレンダリングがスケジュールされる前の古い仮想DOMツリーとを高速に比較し、両者の間でどの部分が変更されたのかを検出する。このプロセスを差分検出(diffing)と呼ぶ。効率的に変更箇所を見つけることで、無駄な更新を防ぎ、高いパフォーマンスを維持する。

最後に、「実際のDOMの必要な部分だけを更新する(調停、reconciliation)」。差分検出の結果、変更が必要とされた部分だけを、Reactはブラウザが描画する実際のDOMに適用する。このプロセスを調停(reconciliation)と呼ぶ。変更があった部分だけを最小限に更新することで、不必要な画面描画処理を避け、パフォーマンスの向上を図っている。もし、DOM全体を毎回再構築していたら、処理が重くなり、ユーザー体験が悪くなってしまうだろう。

この一連のプロセスを経て、ようやく画面上のcountの値が更新され、新しい値が表示されるのである。つまり、setState()は即座にコンポーネントの状態を更新し、画面に反映させるわけではなく、「後で更新する」という指示を出し、現在実行中の処理が終わるのを待ってから、改めて更新処理を開始するという非同期的な性質を持っているのだ。

この挙動を理解することは、Reactで効率的かつバグの少ないアプリケーションを開発する上で非常に重要である。特に、状態更新の直後にその状態の最新の値にアクセスしたい場合は、useEffectフックや、setStateの引数に更新関数を渡すなど、Reactの非同期的な状態更新を考慮した適切な方法を用いる必要がある。今回の記事で述べたように、setState()の直後にconsole.log()で古い値が表示されるのは、Reactがコンポーネントをリレンダリングし、実際のDOMを更新するまでに時間差があるためなのだ。Reactは賢く、効率的に画面を更新するために、状態の変更を即座に反映させるのではなく、スケジュールを組んで処理を進めていることを覚えておくとよいだろう。

関連コンテンツ