FlowingDev

Reactの魔法のショー:コンポーネントが命を吹き込まれる仕組み

Reactが、HTMLのような構文とJavaScriptが混在したJSXを、コンポーネント、state、仮想DOMを通じて、どのようにインタラクティブなWeb UIへと変身させるのか、その秘密を解き明かします。

ツールを試す: React Playground

ひとことで言うと

Reactは、UIを「コンポーネント」という自己完結した再利用可能なコードの部品に分割して作るためのJavaScriptライブラリ。データが変わると、対応するUIが自動で更新されるのが特徴です。

どんな問題を解決するのか

昔々(と言っても2010年代前半くらい)のWeb開発を想像してみてください。Webページがあって、それをインタラクティブにしたいとします。例えば、「いいね」ボタンをクリックしたら青くなって、カウンターの数字が増える、みたいな機能です。jQueryのようなツールを使うと、こんなコードを書くことになります。「ページの中からボタンを見つけて、クリックを監視して、クリックされたらカウンターの要素を見つけてテキストを変更し、それからボタンのアイコンを見つけて色を変えるんだ」と。

これは命令的なアプローチです。ブラウザに、ページをどう変更するかの手順を一つひとつ教えているわけです。いいねボタンくらいなら、これでうまくいきます。でも、何百もの投稿、コメント、通知、ライブチャットウィンドウが全部同時に更新されるような、本格的なSNSのフィードだったらどうでしょう? この「手順書」は、ごちゃごちゃに絡み合ったスパゲッティコードの塊になってしまいます。ある場所での変更が、まったく別の場所を予期せず壊してしまうかもしれません。UIとアプリケーションのデータの一貫性を保つのは、常にエラーの温床となる戦いでした。

Facebookは、この悪夢を大規模に経験していました。2011年、彼らはこの問題を社内で解決するためにReactを開発しました。Reactは宣言的なモデルを導入しました。ブラウザにどうやって変更するかを指示する代わりに、あるstate(状態)に対してUIがどうあるべきかを記述するだけです。「もし投稿がliked状態なら、ボタンは青でカウントは101にすべし。liked状態でなければ、ボタンは灰色でカウントは100にすべし」と宣言するのです。これだけ。likedというstateがfalseからtrueに変わると、Reactがボタンとカウンターを更新するための最も効率的な方法を 알아서 見つけ出してくれます。Reactが「どうやるか」の部分を担ってくれるので、あなたは「何を表示するか」に集中できるのです。

この命令的から宣言的への思考の転換は、革命でした。これによって、複雑でデータ駆動なUIの構築が、より予測可能で、スケーラブルで、デバッグしやすいものになったのです。

舞台裏の仕組み

Reactの「魔法」は本当の魔法ではありません。それは、JSX、Stateを持つコンポーネント、そして仮想DOMという3つの核となるアイデアの賢い組み合わせなのです。

JSX: JavaScriptを甘くコーティングしたもの

Reactのコードを見ると、JavaScriptの中にHTMLのようなものが直接埋め込まれているように見えます。これがJSX(JavaScript XML)です。

const greeting = <h1>Hello, FlowingDev user!</h1>;

あなたのブラウザは、これが何なのかさっぱりわかりません。これは有効なJavaScriptではないからです。このコードが実行される前に、Babelのようなツールによって「トランスパイル」(変換)される必要があります。上記のJSXの行は、通常のJavaScriptの関数呼び出しに変換されます。

const greeting = React.createElement("h1", null, "Hello, FlowingDev user!");

すべてのJSXタグはReact.createElement()という関数の呼び出しになります。この関数呼び出しは、実際のHTML要素を作成しません。それは、あなたが作成したい要素を記述する軽量なJavaScriptオブジェクトを生成します。その型(h1)、プロパティ(この場合はなしなのでnull)、そして子要素("Hello...")です。これらのオブジェクトが、仮想DOMの構成要素となります。

つまり、JSXは単なる「シンタックスシュガー(糖衣構文)」なのです。おなじみのHTMLライクな構文でUIを記述させてくれますが、その裏では、ただJavaScriptオブジェクトのツリーを作っているだけなのです。

コンポーネントとState: 司令塔となる部分

コンポーネントとは、何らかのJSXを返すJavaScriptの関数のことです。再利用可能で自己完結したUIの部品です。

function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

このGreetingコンポーネントはシンプルです。nameを入力(「prop」と呼ばれます)として受け取り、見出しをレンダリングします。しかし、カウンターのように、何かを記憶して時間と共に変化するコンポーネントが欲しくなったらどうでしょう? そのためには「state」が必要です。

Stateは、コンポーネントが「所有」し、変更することができるデータです。Reactはこれを管理するために「フック」と呼ばれる特別な関数を提供しています。最も一般的なのがuseStateです。

import { useState } from 'react';

function Counter() {
  // "count"が我々のstate変数。
  // "setCount"が、それを更新するために使える*唯一*の関数。
  const [count, setCount] = useState(0); // 初期値は0。

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

setCount(count + 1)を呼び出すとき、あなたはただ変数を変更しているわけではありません。Reactにこう伝えているのです。「ねえ、このCounterコンポーネントのstateが変わったよ。この関数をもう一度実行して新しいUIを取得して、画面を更新してくれたまえ」と。するとReactはコンポーネントを「再レンダリング」し、更新されたcountを含む新しいJSXを取得し、効率的にWebページを更新します。

仮想DOM: Reactの秘密兵器

実際のブラウザのDOM(生きているHTML要素のツリー)を直接変更するのは遅い処理です。それは、額縁を一つ動かしたいだけなのに、部屋の壁を全部壊して再建するようなものです。

Reactは、その秘密兵器である**仮想DOM(Virtual DOM、VDOM)**によって、このコストの高い作業を回避します。VDOMは、JavaScriptのメモリ内に保持されているDOMの単なるコピーです。先ほど話したReact.createElementオブジェクトのツリーがそれです。

このプロセスは**reconciliation(調整)**として知られています。

  1. 初回レンダー: Counterが最初にレンダリングされると、Reactは「You clicked 0 times」というテキストを持つpとbuttonを含むdivを記述したVDOMツリーを構築します。そして、これを使ってページ上に実際のDOM要素を作成します。
  2. Stateの変更: あなたがボタンをクリックします。setCount(1)が呼ばれます。
  3. 再レンダーと新しいVDOM: ReactはCounter関数を再実行します。そして新しいVDOMツリーを構築します。この新しいツリーは古いものとほとんど同じですが、pは「You clicked 1 times」というテキストを記述するようになっています。
  4. 差分検出(Diffing): Reactの「差分検出」アルゴリズムが、新しいVDOMツリーと古いものを比較します。両者を駆け巡り、違いを見つけ出します。この場合、見つかる違いはただ一つ、<p>タグ内のテキストが変わったことだけです。
  5. 一括更新: Reactはこの小さな変更点だけを実際のDOMに適用します。divやbuttonには触れず、pのテキストだけを更新します。

この差分検出とパッチ適用のプロセスは、信じられないほど高速です。更新をまとめて行い、遅い実際のDOMへの直接的な操作を最小限に抑えることで、Reactは複雑なデータが急速に変化しているときでさえ、UIをキビキビとレスポンシブに感じさせるのです。

実際のやらかし事例

フォームデータ消失事件

ある若手開発者が、複数ステップからなる複雑なユーザー登録フォームの構築を任されました。各ステップはそれぞれ異なるコンポーネントです。ある時、ステップ2に小さなチェックボックスを置いて、条件によって追加の入力フィールドを表示する必要が出てきました。彼はそれを実装しましたが、突如として奇妙なバグが発生しました。ユーザーがそのチェックボックスにチェックを入れると、ステップ2で入力したデータがすべて消えてしまうのです。

犯人は「key」の理解不足でした。追加フィールドを表示するために、開発者はJSXの構造を、Reactがステップ2のフォームコンポーネント全体を新しくて別のコンポーネントだと勘違いするような方法で変更してしまっていたのです。それを識別するための安定したkey propがなかったため、Reactは古いコンポーネント(とそのstate、つまりユーザーの入力データすべて)を破棄し、代わりにまっさらな新しいコンポーネントをマウントしてしまったのです。

教訓: key propはリストのためだけにあるのではありません。コンポーネントにとっての安定したIDです。Reactがレンダリングをまたいでコンポーネントを追跡するためにkeyをどう使うかを理解することは、stateを保持し、予期せぬリセットを避けるために極めて重要です。

無限APIハンマー事件

あるチームが、ライブの株価データを表示するダッシュボードを構築していました。ある開発者が、useEffectフックを使って特定の銘柄の最新価格を取得するコンポーネントを書きました。コードはこんな感じでした。

useEffect(() => {
  fetchStockPrice(stockSymbol).then(price => {
    setPrice(price); // stateを新しい価格で更新
  });
}); // おっと、依存配列(dependency array)がない!

コンポーネントがロードされた途端、アプリはフリーズし、開発者のノートPCのファンは唸りを上げ、ネットワーク監視ツールは毎秒何千もの同じAPIリクエストが発射されているのを示しました。彼らは無限ループを生み出してしまったのです。useEffectがデータを取得してsetPriceを呼び出します。stateの更新がコンポーネントの再レンダリングを引き起こします。useEffectには依存配列がなかったので、すべてのレンダリングの後に再び実行され、再びデータを取得し、再びstateを更新し、再び再レンダリングを引き起こし…それが永遠に続いたのです。

教訓: useEffectのようなReactフックは強力ですが、精密なツールです。いつエフェクトを再実行するかをReactに伝えるために「依存配列」を渡さなければなりません。それを省略すると「毎回のレンダリング後に実行する」という意味になり、stateの更新と組み合わせると大惨事の元になります。

よくある間違いや落とし穴

  • stateを直接変更してしまう。 myState.prop = 'value'やmyArray.push('item')のようなことは決してしないでください。Reactの変更検出は、オブジェクトや配列の同一性に依存しています。setStateを呼び出すときは、Reactが変更に気づけるように、新しいオブジェクトや配列を提供しなければなりません(例:スプレッド構文 ... を使う)。
  • リストでkeyを忘れる。 .map()を使ってアイテムのリストをレンダリングするときは、各要素にユニークで安定したkey prop(item.idなど)を与えなければなりません。これはReactの差分検出アルゴリズムが、どのアイテムが移動したか、追加されたか、削除されたかを効率的に追跡するための方法です。配列のインデックスをkeyとして使うのは、バグにつながる可能性のある一般的なアンチパターンです。
  • JSXをHTMLと同じように扱ってしまう。 似ていますが、同じではありません。classの代わりにclassNameを、forの代わりにhtmlForを、そしてスタイルのプロパティにはキャメルケース(background-colorではなくbackgroundColor)を使う必要があります。また、すべてのタグは閉じる必要があるので、画像は<img>ではなく<img ... />となります。
  • フックを魔法だと思ってしまう。 useStateやuseEffectのようなフックは厳格なルールに従います(例:関数コンポーネントのトップレベルでしか呼び出せない)。これらは、レンダリング間で一貫した呼び出し順序に依存して動作します。ループや条件分岐の中で呼び出そうとすると、アプリは壊れてしまいます。

なぜ注目すべきか

Reactは単なるライブラリではありません。それはエコシステムであり、一つの考え方です。InstagramやNetflixのような巨大サイトから、無数のスタートアップや社内ツールまで、現代のWebの大部分を支えています。Reactを学ぶということは、再利用可能で、テスト可能で、保守性の高いコードを促進するコンポーネントベースのアーキテクチャを学ぶということです。

たとえ日常的にReactを使わなくても、その中心的な原則(宣言的UI、state管理、仮想DOM)を理解すれば、より良い開発者になれるでしょう。これらのコンセプトは、VueからSvelteまで、他のほとんどすべての現代的なUIフレームワークに影響を与えています。さらに、React Nativeを使えば、同じスキルと原則でiOSとAndroidのネイティブモバイルアプリを構築できます。これは、今日ユーザーインターフェースに取り組むどんな開発者にとっても不可欠な、強力なパラダイムなのです。

もっと深く知るために

  • React Docs (Official) - 公式ドキュメントが最高の出発点です。包括的なチュートリアルと詳細な解説があります。
  • Introducing JSX (React Docs) - JSXとは何か、HTMLとどう違うのかについての公式な説明です。
  • Reconciliation (React Docs) - Reactが2つのVDOMツリーの差分を検出し、UIを効率的に更新するために使用するアルゴリズムです。
  • The Rules of Hooks (React Docs) - useStateやuseEffectのようなフックを使う際に従わなければならない必須のルールです。
  • MDN: Introduction to the DOM - Reactが何を操作しているのかを理解するためには、Document Object Model自体を理解することが不可欠です。

エディターはどうコードを理解するのか

色分けはプログラムの実行ではなく、言語パーサーによるものです。CodeMirror と Lezer がブラウザー内で記号と構文を解析します。候補にはキーワード、対応する場合のローカル名、スニペット、小さな組み込みリファレンスが含まれます。登録済みのシンボルにカーソルを重ねるとシグネチャが表示されます。候補が出ても、その API が実行環境に存在する保証はありません。対応するパーサーは構文エラーを検出しますが、型検査や実行の代わりにはなりません。Ruby と Lua はトークンベースのモードで、診断は限定的です。リモートの言語サーバーは使わず、コードは端末内に留まります。

CodeMirror & Lezer: MIT.

理論はOK。さあ手を動かそう — 100%ブラウザ内で。

ツールを試す: React Playground