コンポーネント・UI

React vs Web Components

React と Web Components のコンポーネントモデル、スタイル隔離、状態管理、ライフサイクルを比較。それぞれの強みと適切な使い分けを、コード例とともに解説。

比較一覧

観点 React Web Components
コンポーネント定義 関数 + JSX を返す クラス + HTMLElement を継承
スタイル隔離 CSS Modules / CSS-in-JS Shadow DOM (ブラウザネイティブ)
状態管理 useState / useReducer 属性 + プロパティ + 手動更新
レンダリング 仮想 DOM (差分更新) ネイティブ DOM (直接操作)
ライフサイクル useEffect (同期モデル) connectedCallback (コールバックモデル)
エコシステム React 専用ライブラリ群 フレームワーク非依存
命名規則 大文字で始まる (MyButton) ハイフン必須 (my-button)

ランタイムとバンドルサイズ

React
~100 KB
react + react-dom (gzip 後、v19 基準)

ランタイムが必須。仮想 DOM エンジン、イベントシステム、フック機構を含む。フレームワーク自体のコードがバンドルに追加される。

Web Components
0 KB
ブラウザ組み込み API

追加のランタイム不要。Custom Elements, Shadow DOM, HTML Templates はすべてブラウザに組み込まれている。コンポーネントのコードだけがバンドルに含まれる。

コンポーネント定義

React
// 関数がコンポーネント
function Greeting({ name }) {
  return <h1>こんにちは、{name}!</h1>;
}

// 使用
<Greeting name="Alice" />
Web Components
// クラスを定義してブラウザに登録
class GreetingElement extends HTMLElement {
  connectedCallback() {
    const name = this.getAttribute('name');
    this.innerHTML = `<h1>こんにちは、${name}!</h1>`;
  }
}
customElements.define('x-greeting', GreetingElement);

// 使用
<x-greeting name="Alice"></x-greeting>

React は関数が JSX を返すだけでコンポーネントになります。Web Components はクラスを定義してブラウザに登録する必要があります。React のコンポーネント名は大文字で始まる必要があり、Web Components の要素名にはハイフンが必須です。

スタイル隔離

React (CSS Modules)
/* Card.module.css */
.card { border: 1px solid #ddd; padding: 1rem; }
.title { font-weight: bold; }

/* Card.jsx */
import styles from './Card.module.css';

function Card({ title, children }) {
  return (
    <div className={styles.card}>
      <h2 className={styles.title}>{title}</h2>
      {children}
    </div>
  );
}
ツール依存

ビルドツールがクラス名をユニーク化(例: Card_card__x7f2a)することでスコープを実現。ブラウザ自体は関与しない。

Web Components (Shadow DOM)
class MyCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        .card { border: 1px solid #ddd; padding: 1rem; }
        .title { font-weight: bold; }
      </style>
      <div class="card">
        <h2 class="title"><slot name="title"></slot></h2>
        <slot></slot>
      </div>
    `;
  }
}
customElements.define('my-card', MyCard);
ブラウザネイティブ

Shadow DOM がスタイルを完全に隔離。ビルドツール不要。.card は Shadow DOM 内でのみ有効。

React の CSS Modules はビルド時にクラス名をユニーク化(例: Card_card__x7f2a)してスコープを実現します。Web Components の Shadow DOM はブラウザネイティブの仕組みでスタイルを完全に隔離します。どちらも「コンポーネントのスタイルが外部に漏れない」という同じ目的ですが、実現方法が根本的に異なります。

状態管理とリアクティビティ

React
import { useState } from 'react';

function Counter() {
  // 状態を宣言 → 変更で自動再レンダリング
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      クリック数: {count}
    </button>
  );
}
Web Components
class MyCounter extends HTMLElement {
  #count = 0;

  connectedCallback() {
    this.render();
    // イベントリスナーを手動で登録
    this.addEventListener('click', () => {
      this.#count++;
      this.render(); // 手動で DOM 更新
    });
  }

  render() {
    this.innerHTML =
      `<button>クリック数: ${this.#count}</button>`;
  }
}
customElements.define('my-counter', MyCounter);

React は useState で状態を宣言し、状態変更時に自動で再レンダリングします。開発者は「状態が変わったら UI を再計算」と宣言するだけで、DOM の更新は React が自動で行います。Web Components では状態変更の検知も DOM 更新も手動で行う必要があります。

ライフサイクル

React
1 レンダーフェーズ 純粋・副作用なし

関数コンポーネントを実行し、JSX を返す。仮想 DOM を生成して前回との差分を計算。

2 コミットフェーズ DOM 更新

差分に基づいて実際の DOM を更新。

useLayoutEffect DOM 更新直後、描画前に同期実行
▼ ブラウザ描画
useEffect 描画後に非同期実行(メイン)
3 更新サイクル setState / props

state や props が変更されると、レンダーフェーズからやり直し。前回の effect のクリーンアップ → 新しい effect の実行。

4 アンマウント

全ての effect のクリーンアップ関数を実行。

useState 状態を保持・更新
useEffect 副作用(API 通信等)
useMemo 計算結果のメモ化
useRef 再レンダーなしで値を保持
useCallback 関数のメモ化
useContext コンテキストを購読
Web Components
1 constructor()

要素を生成。super() の呼び出し、Shadow DOM の作成、初期状態の設定を行う。属性や子要素にはまだアクセスできない。

2 connectedCallback()

DOM に追加された時。レンダリング、イベントリスナーの登録、データの取得を行う。複数回呼ばれる可能性がある(要素の移動時)。

3 attributeChangedCallback()

observedAttributes で指定した属性が変更された時。name, oldValue, newValue の 3 引数を受け取る。React の props 変更に相当するが、手動で DOM を更新する必要がある。

4 disconnectedCallback()

DOM から削除された時。イベントリスナーの解除、タイマーのクリア、外部リソースの解放を行う。React の cleanup 関数に相当。

React は仮想 DOM の差分計算、描画前後のフック、メモ化など多層的な仕組みを持ちます。一方 Web Components は 4 つのシンプルなコールバックで完結します。React のほうが複雑ですが、その分パフォーマンス最適化のツールが豊富です。Web Components は直接 DOM を操作するため、最適化は開発者の責任になります。

どちらを選ぶか

React が適している場面

  • 複雑な状態管理を持つ SPA
  • React エコシステム(Next.js, Remix 等)を活用したい
  • チームが React に習熟している
  • 仮想 DOM による効率的な差分更新が必要

Web Components が適している場面

  • フレームワーク非依存のデザインシステム
  • React・Vue・Svelte 間で共有する UI
  • CMS や静的 HTML での利用
  • マイクロフロントエンド
組み合わせも可能

React アプリ内で Web Components を使うことも、Web Components 内に React をマウントすることも可能です。デザインシステムを Web Components で構築し、各チームが好きなフレームワークで利用するパターンは実務で増えています。

Props vs 属性(Attributes)

React は Props として JavaScript のあらゆる値(オブジェクト、配列、関数)を渡せます。Web Components の HTML 属性は文字列のみ。複雑なデータはプロパティ経由で渡す必要があります。

React
// あらゆる値を Props で渡せる
<UserList
  users={[{ name: "Alice" }, { name: "Bob" }]}
  onSelect={(user) => console.log(user)}
  config={{ theme: "dark", limit: 10 }}
/>
Web Components
<!-- 属性は文字列のみ -->
<user-list theme="dark" limit="10"></user-list>

<script>
// 複雑なデータはプロパティ経由
const el = document.querySelector('user-list');
el.users = [{ name: "Alice" }, { name: "Bob" }];
el.onSelect = (user) => console.log(user);
</script>
⚠️

React から Web Components を使う場合、React 19 以降はプロパティを自動的に設定します。React 18 以前では ref を使って手動でプロパティを設定する必要があります。

イベント処理

React は合成イベント(SyntheticEvent)でブラウザ差異を吸収します。Web Components はネイティブの CustomEvent を使い、Shadow DOM 境界でのイベント伝播に注意が必要です。

React
function SearchBox({ onSearch }) {
  const [query, setQuery] = useState('');

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      onSearch(query);  // コールバック Props
    }}>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
    </form>
  );
}

// 使用: 関数を直接渡す
<SearchBox onSearch={(q) => fetch(`/api?q=${q}`)} />
Web Components
class SearchBox extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `<form><input type="text"/></form>`;

    shadow.querySelector('form')
      .addEventListener('submit', (e) => {
        e.preventDefault();
        const query = shadow.querySelector('input').value;
        // CustomEvent で外部に通知
        this.dispatchEvent(new CustomEvent('search', {
          detail: { query },
          bubbles: true,     // 親要素に伝播
          composed: true     // Shadow DOM 境界を越える
        }));
    });
  }
}

// 使用: addEventListener で購読
document.querySelector('search-box')
  .addEventListener('search', (e) => {
    fetch(`/api?q=${e.detail.query}`);
  });
composed: true — Shadow DOM 境界を越えてイベントを伝播させるために必要。React にはこの概念がない。