はじめに
こんにちは、山本です。
今回は、自主学習をする中でReactなどのJavaScriptフレームワークの利便性について具体的に理解する経験をしたので当記事で紹介します。
ReactなどのJavaScriptフレームワークはしばしば 「JavaScriptでの面倒な実装を簡単にやってくれる優れもの」 と表現されることがあり、私自身も初学習時には前述のように教わり「理解した気」になっていました。
しかし、具体的にどう優れているのかは今ひとつピンとこない方も多いと思います(私もです)。
当記事では、主にReactというJavaScriptフレームワークの利便性をあくまで一例として「宣言的」というキーワードを軸に、JavaScriptでの実装と比較しながら深掘りしていきたいと思います。
JavaScriptの「手続的」なスタイル
まずはJavaScriptの「手続的」なスタイルについての懸念点を実際のコードを見ながら解説します。
ここで、手続的なスタイルとは、「どのように処理するか」を命令的に細かく記述するスタイルのことを指しています。
※ プログラミングの勉強をこの「手続的」なスタイルから始めた方も多いのではないかと思います。
例えば、countという変数をクリックごとに増やし、画面に表示するシンプルなカウンターアプリを考えます。
コードは以下の通りです。
let count = 0; const button = document.getElementById('myButton'); const display = document.getElementById('countDisplay'); button.addEventListener('click', () => { count = count + 1; // 画面の表示を更新するため、DOMを直接操作する必要がある display.textContent = count; });
このコードでの懸念点は、変数への数値代入の処理と実際のページ内での表示(DOM:Document Object Modelと呼ばれます)の処理が別々に定義されていることです。
この状態(スクリプト内部の変数の状態と実際のページ表示の動きが分離している状態)だと、変数自体の値が変わるたびにdisplay.textContent
の記載を書き換える必要があり、要素が増えたり画面が複雑になればなるほど手間が増え、バグの温床となってしまいます。
また、そもそもDOM処理の記載を忘れてしまうこともバグの増加につながる可能性があります。
JavaScript上での状態保持は、上記のようにエンジニアの手間を増やしかねない不便さがあります。
Reactの「宣言的」なスタイル
一方、ReactではuseStateフックという「宣言的」の概念を用いて状態を管理します。
宣言的なスタイルとは、「どうあるべきか」を記述するだけで、細かな処理はフレームワークなどが自動で行ってくれるスタイルです。
同じカウンターアプリでの処理部分をReactで実装してみましょう。
※ はてなブログにおけるコードブロックのコードハイライトがJSXには非対応なため、シンタックスハイライトは無しにしています。
const Counter = () => { const [count, setCount] = useState(0); const increment = () => { setCount(prevCount => prevCount + 1) }; return ( <div> { /*countDisplay*/ } <span> {count} </span> { /*myButton*/ } <button onClick={increment}>カウントアップ</button> </div> ); }
「宣言的」なReactの利便性は、const [count, setCount] = useState(0);
の一行に集約されています。
具体的には、count
という「状態変数」を定義し、setCount
という「関数」で状態変数count
更新するとReactが画面を再レンダリングする(DOM処理を自動で行ってくれる)という処理ルールを定義してくれます。
したがって、前述したJavaScriptの懸念点で挙げていた手間を解消してくれるので エンジニア側としては大変便利 なワケです。
また、実際にコードを書く際に処理を一行にまとめて記述できるので、Reactでの宣言的なコード記述の場合は スクリプト内部の変数の状態と実際のページ内での表示が直感的に結びつく ので扱いやすいと個人的には感じました。
宣言的なスタイル:useStateの注意点
今回「宣言的なスタイル」の一例として紹介したuseStateはとても便利な機能ですが、重要な注意点が存在します。
それは、 useStateが非同期で実行される ということです。
※ここでいう「非同期」の定義は、setCountなどの状態更新関数を呼び出しても、すぐにその場で状態(count)が更新されるわけではなく、Reactが次のレンダリングタイミングでまとめて状態を更新するという意味合いになっています。
先ほどのセクションで紹介したコード内の処理setCount(prevCount => prevCount + 1)
は以下の参考コードのようにシンプルなものに書き換えることができます。
let count = 0; setCount(count + 1); setCount(count + 1);
上記コードであれば、count
に0が代入された後、setCount
を使用して1ずつ2回加算を行なっているように見えます。
しかし、実際に処理後のcount
の値を出力すると、上記コードでは1が表示されます。
この現象は、useState特有の命令実行方法が起因しています。
useStateでは、「キュー」という処理単位を使って処理を行います。
そして、このキューに登録された複数の処理をページのレンダリング処理時にまとめて行うという特性があります。
※ 通常はsetCount
を一度使用したタイミングでページの再レンダリングは行われますが、今回のケースのようにsetCount
が連続しているときには、Reactは状態更新をまとめて処理するという動きになります。
さらに、参考コードのsetCount(count + 1);
では、setCountを呼び出した時点のCountの値に依存します。(直接更新と呼びます)
つまり、 2つのsetCountを同時に実行することで、以下のような現象が起こってしまいます。
//キューの中身の処理をレンダリング時に一括実行する setCount(count + 1); // ここで count は 0 なので、setCount(1) が呼ばれるが、count変数自体の更新は実行されない。 setCount(count + 1); // count は更新されておらず、まだ 0 なので、setCount(1) が呼ばれる。
上記のような現象を回避するため、 useStateで値を更新するときには以下のようなコードを使用します。
let count = 0; setCount(prevCount => prevCount + 1) ; setCount(prevCount => prevCount + 1) ;
記載したコードでは、setCount
で登録するキューの中身をcount + 1
のような「直接的な式」ではなく、prevCount => prevCount + 1
という「関数」を登録しています。
この「関数更新」を利用することによって、単一の処理内におけるキューに処理の時系列的な意味合いを持たせることができます。
具体的には、以下のような処理の流れになります。
1. setCount(prevCount => prevCount + 1) がキューに追加される 2. setCount(prevCount => prevCount + 1) が再びキューに追加される // 実行時 1. 最新の count (0) が prevCount に渡され、0 + 1 = 1 に更新 2. 最新の count (1) が prevCount に渡され、1 + 1 = 2 に更新
この方法であれば、setStateが複数回呼ばれても、それぞれの更新が前の更新結果に確実にアクセスできるため、意図した通りの順番で状態が更新されます。
おわりに
今回は、JavaScriptフレームワークの利便性を、一例として「宣言的」というキーワードを軸にJavaScriptでの実装と比較しながら深掘りしてみました。
個人的には、この宣言的なスタイルの実装時にあまりの便利さに感動しました。(そのすぐ後に注意点で取り上げた問題に見事にハマりましたが。。。)
フレームワークを使えばとても実装が楽にはなりますが、前述のように便利さの裏には重要な注意点も含まれていることが多いのでそのような注意点を考慮して実装を進めていくことが大切だということが今回の大きな学びです。
この記事を読むことによってReact等のJavaScriptフレームワークにおける利便性が少しでも明確になれば幸いです。
山本 竜也 (記事一覧)
2025年度新入社員です!AWSについてはほぼ未経験なのでたくさんアウトプットできるよう頑張ります✨