Skip to content

1.3 命令的 UI の限界 — 大規模 SPA で起きた問題 ​

ここは Part 1 の中でも 一番の山場 です。ここを越えると、「React がなぜこういう形をしているのか」が腑に落ちるようになります。急がず、一緒に見ていきましょう。

まず、この章で伝えたいことを 1 行で言います。

大規模 SPA では、「イベントが起きたら DOM を直接書き換える」スタイルが崩れやすくなり、React はその「崩れやすさ」を設計で解消しようとしました。

前章 1.2 DOM を直接触る世界 で見たとおり、jQuery 時代の UI 開発は命令的なスタイルが主流でした。この章では、そのスタイルが大規模 SPA で具体的に何を困らせたのか、そして当時 Facebook エンジニアリングチームがどのように問題を捉え直したのかを、React 公式の一次ブログから追います。

命令的スタイルが壊れる典型パターン ​

Facebook のようなサイズのアプリでは、次のような UI が無数に存在します。

  • ニュースフィード上で「いいね」を押すと、該当の投稿のいいね数・ボタンの色・通知バッジが同時に変わる
  • チャットのメッセージが届くと、サイドバーの未読バッジ、タブのタイトル、中央のスレッド、お知らせ音の有無が連動する
  • 設定画面で項目を変更すると、ヘッダー / サイドバー / 本文の複数箇所が同時に更新される

これらを 個別のイベントハンドラから DOM を直接いじる スタイルで書くと、次の 3 つが噴き出します。初学者がつまずきやすいところなので、少しだけ丁寧に並べます。

  1. 同じ状態を複数の場所で描いてしまう — 未読数が「サイドバー」「タブタイトル」「通知ベル」の 3 箇所にある場合、3 つを同期して書き換えるコードを全部のイベントに書かないといけません。1 箇所を書き忘れると、UI の整合性が壊れます。
  2. 遷移が多い — 状態 A から B に変わったとき、何を足して、何を消して、何を色変えするか を遷移ごとに書く必要があります。状態が n 個あれば遷移は最悪 n × (n-1) 個。
  3. DOM が信頼できない — 画面に出ている DOM が「正しい状態」の単一源ではなくなります。ある要素のテキストと、別の要素の属性と、JavaScript の変数が全部食い違っている、という現象が起きます。

図 1.3.1 状態が複数箇所に散らばる苦しみ ​

真の状態未読数 = 3手で書き換え手で書き換え手で書き換えサイドバーバッジに "3" を書くタブタイトル"(3) 受信箱" に書く通知ベルドット表示を有効化どれか 1 つでも書き忘れると UI が嘘をつく

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 render is 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) ​

html
<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 で本格的に扱うので、今は雰囲気だけ)。

tsx
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 の動機 に進んでください。

参照ソース ​