1.3 命令的 UI の限界 — 大規模 SPA で起きた問題
ここは Part 1 の中でも 一番の山場 です。ここを越えると、「React がなぜこういう形をしているのか」が腑に落ちるようになります。急がず、一緒に見ていきましょう。
まず、この章で伝えたいことを 1 行で言います。
大規模 SPA では、「イベントが起きたら DOM を直接書き換える」スタイルが崩れやすくなり、React はその「崩れやすさ」を設計で解消しようとしました。
前章 1.2 DOM を直接触る世界 で見たとおり、jQuery 時代の UI 開発は命令的なスタイルが主流でした。この章では、そのスタイルが大規模 SPA で具体的に何を困らせたのか、そして当時 Facebook エンジニアリングチームがどのように問題を捉え直したのかを、React 公式の一次ブログから追います。
命令的スタイルが壊れる典型パターン
Facebook のようなサイズのアプリでは、次のような UI が無数に存在します。
- ニュースフィード上で「いいね」を押すと、該当の投稿のいいね数・ボタンの色・通知バッジが同時に変わる
- チャットのメッセージが届くと、サイドバーの未読バッジ、タブのタイトル、中央のスレッド、お知らせ音の有無が連動する
- 設定画面で項目を変更すると、ヘッダー / サイドバー / 本文の複数箇所が同時に更新される
これらを 個別のイベントハンドラから DOM を直接いじる スタイルで書くと、次の 3 つが噴き出します。初学者がつまずきやすいところなので、少しだけ丁寧に並べます。
- 同じ状態を複数の場所で描いてしまう — 未読数が「サイドバー」「タブタイトル」「通知ベル」の 3 箇所にある場合、3 つを同期して書き換えるコードを全部のイベントに書かないといけません。1 箇所を書き忘れると、UI の整合性が壊れます。
- 遷移が多い —
状態 A から B に変わったとき、何を足して、何を消して、何を色変えするかを遷移ごとに書く必要があります。状態が n 個あれば遷移は最悪 n × (n-1) 個。 - DOM が信頼できない — 画面に出ている DOM が「正しい状態」の単一源ではなくなります。ある要素のテキストと、別の要素の属性と、JavaScript の変数が全部食い違っている、という現象が起きます。
図 1.3.1 状態が複数箇所に散らばる苦しみ
Facebook の答え: 公式ブログ "Why did we build React?" (2013-06-05)
React は 2013 年 5 月 29 日に Facebook のエンジニア Jordan Walke によってオープンソース化されました。その直後の 2013 年 6 月 5 日、React チームの Pete Hunt が React の legacy ブログに "Why did we build React?" を投稿しています。これは React の設計思想を当事者の言葉で理解できる一次資料で、本サイトでは何度も参照します。
このブログで React はこう位置付けられています。
React is a library for building composable user interfaces. It encourages the creation of reusable UI components which present data that changes over time.
— Pete Hunt, "Why did we build React?" (2013-06-05)
そして、「React が輝くのは、データが時間とともに変わるとき」と続きます。
React really shines when your data changes over time.
— 同上
React の 2 つの発想
当時のブログ記事から、React の発想の核を 2 点抜き出します。
(1) テンプレートではなく、再利用可能なコンポーネント
Many developers find it useful to separate the templating language from the code that plugs data into markup. React approaches building user interfaces differently by breaking them into components.
— 同上 (要旨)
当時は HTML テンプレート言語 (Handlebars、Mustache など) が広く使われていました。React はそれとは逆に、ロジックとマークアップを同じコンポーネントの中に閉じ込める という判断をしています。この判断は後の JSX (Part 3.2 で扱う) に繋がります。
(2) データが変わったら再描画してしまう
Reactive updates are dead simple. Let's use a magnet metaphor. If you've ever used a framework like Backbone, you've had to tell a view to listen to events, and then manually update another view based on those events. React's diffing algorithm takes the pain out of this: when state changes, React knows which parts of the DOM to update.
— 同上 (要旨)
ここが、命令的スタイルとの決定的な違いです。React は開発者に「データが変わったら、UI 全体を最初から描画し直す関数」を書かせます。DOM に差分を当てる仕事は React 側 が引き受けます。
React 公式ブログは、出力される中間物について次のように書いています。
The data returned from
renderis neither a string nor a DOM node — it's a lightweight description of what the DOM should look like.— 同上
この「render の返り値は DOM でもなければ文字列でもない、DOM がどうあるべきかの軽量な記述である」という表現が、後に Virtual DOM と呼ばれるようになります (Part 6.1 で詳しく扱います)。当時のブログ自体は "Virtual DOM" という用語を使わず、仕組みの方を素直に述べています。
宣言的と命令的、その違いを 1 つのコードで見る
差が一番出るのが、次のようなシンプルなカウンターです。最初は違和感があるかもしれませんが、「何をどう書いているか」を読み比べてみてください。
命令的スタイル (jQuery)
<button id="btn">いいね (<span id="count">0</span>)</button>
<div id="status"></div>
<script>
let count = 0;
$("#btn").on("click", function () {
count += 1;
$("#count").text(count);
if (count >= 10) {
$("#status").text("Thanks!");
$("#btn").prop("disabled", true);
}
});
</script>count の現在値を参照しつつ、#count / #status / #btn の どの DOM を、どう書き換えるか をハンドラの中に全部書いています。状態遷移が増えるほど、このハンドラは肥大化します。
宣言的スタイル (React、参考)
同じ要件を React 風に書くとこうなります (Part 3 で本格的に扱うので、今は雰囲気だけ)。
function Liker() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)} disabled={count >= 10}>
いいね ({count})
</button>
<div>{count >= 10 ? "Thanks!" : ""}</div>
</>
);
}count が変わったら React が再度この関数を呼び、返り値の通りに DOM を書き換えてくれます。「この状態なら UI はこうあるべき」を 1 箇所に書く のが宣言的スタイルです。遷移ごとの DOM 操作を書く必要はありません。
ページ上で動くデモ
下のデモは、同じ「いいねボタン (10 で Thanks!)」を左 / 右で並べたものです。左 (命令的) は、ハンドラの中で「count を書く」「status を書く」「disabled を書く」と 3 つの DOM 操作を毎回書いています。右 (宣言的) は、ハンドラは count を 1 増やすだけで、見た目は状態から自動で決まります。
ボタンを押して、ログの行数の違いを見比べてみてください。
jQuery 風- (ボタンを押すと、ハンドラが書く DOM 操作が表示されます)
ハンドラが 見た目を 3 箇所 同期して書き換えています。 状態が増えると、この書き換え分岐がどんどん膨らみます。
React 風- (ボタンを押すと、状態の動きが表示されます)
ハンドラは count を 1 増やすだけ。「10 以上なら disabled」「10 以上なら Thanks!」の見た目は、状態から計算される JSX が自動で決めます。
図 1.3.2 命令的 vs 宣言的
なぜ「再描画しても速い」が成立するのか
「データが変わるたびに UI を全部作り直すなら遅いのでは?」という疑問が当然出ます。最初は違和感があるところだと思います。React 公式ブログは、この点にも触れています。
- React の
renderは実際の DOM を作るのではなく、軽量な記述 (後の Virtual DOM) を作る - 前回の記述と今回の記述を React 側で差分比較し、変わった箇所だけ を実際の DOM に反映する
- DOM 操作はブラウザの中でも比較的重い処理で、差分最小化は大きな効果を持つ
この仕組みを Reconciliation (調停) と呼び、Part 6 でより詳しく見ます。ここでは「開発者は宣言的に書けて、React が内部で命令的に最適化する」という役割分担だけ押さえてください。
この章のまとめ
- 大規模 SPA で命令的スタイルは崩れます。「何が真実か」が DOM のあちこちに散らばるため。
- React は 2013 年 6 月のブログで、再利用可能なコンポーネント と データが変わったら UI 全体を記述し直す という 2 つの発想を打ち出しました。
renderの返り値は DOM ではなく、DOM の軽量な記述 (後の Virtual DOM) です。- 開発者は宣言的に書き、React が差分当てを担います。この役割分担が Reconciliation です。
次章では、同じ公式ブログをもとに React の登場経緯 (Facebook 内部での使用、JSConf での公開) をもう少し踏み込んで見ていきます。
1.4 React の登場 (2013) — Facebook の動機 に進んでください。
参照ソース
- React 公式ブログ (legacy): Why did we build React? (2013-06-05, Pete Hunt)
- Wikipedia — React (software)(Jordan Walke、2013-05-29 初公開の日付の確認)