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) |
ランタイムとバンドルサイズ
ランタイムが必須。仮想 DOM エンジン、イベントシステム、フック機構を含む。フレームワーク自体のコードがバンドルに追加される。
追加のランタイム不要。Custom Elements, Shadow DOM, HTML Templates はすべてブラウザに組み込まれている。コンポーネントのコードだけがバンドルに含まれる。
コンポーネント定義
// 関数がコンポーネント
function Greeting({ name }) {
return <h1>こんにちは、{name}!</h1>;
}
// 使用
<Greeting name="Alice" /> // クラスを定義してブラウザに登録
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 の要素名にはハイフンが必須です。
スタイル隔離
/* 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)することでスコープを実現。ブラウザ自体は関与しない。
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 はブラウザネイティブの仕組みでスタイルを完全に隔離します。どちらも「コンポーネントのスタイルが外部に漏れない」という同じ目的ですが、実現方法が根本的に異なります。
状態管理とリアクティビティ
import { useState } from 'react';
function Counter() {
// 状態を宣言 → 変更で自動再レンダリング
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
クリック数: {count}
</button>
);
} 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 更新も手動で行う必要があります。
ライフサイクル
関数コンポーネントを実行し、JSX を返す。仮想 DOM を生成して前回との差分を計算。
差分に基づいて実際の DOM を更新。
useLayoutEffect DOM 更新直後、描画前に同期実行 useEffect 描画後に非同期実行(メイン) state や props が変更されると、レンダーフェーズからやり直し。前回の effect のクリーンアップ → 新しい effect の実行。
全ての effect のクリーンアップ関数を実行。
useState 状態を保持・更新 useEffect 副作用(API 通信等) useMemo 計算結果のメモ化 useRef 再レンダーなしで値を保持 useCallback 関数のメモ化 useContext コンテキストを購読 constructor() 要素を生成。super() の呼び出し、Shadow DOM の作成、初期状態の設定を行う。属性や子要素にはまだアクセスできない。
connectedCallback() DOM に追加された時。レンダリング、イベントリスナーの登録、データの取得を行う。複数回呼ばれる可能性がある(要素の移動時)。
attributeChangedCallback() observedAttributes で指定した属性が変更された時。name, oldValue, newValue の 3 引数を受け取る。React の props 変更に相当するが、手動で DOM を更新する必要がある。
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 属性は文字列のみ。複雑なデータはプロパティ経由で渡す必要があります。
// あらゆる値を Props で渡せる
<UserList
users={[{ name: "Alice" }, { name: "Bob" }]}
onSelect={(user) => console.log(user)}
config={{ theme: "dark", limit: 10 }}
/> <!-- 属性は文字列のみ -->
<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 境界でのイベント伝播に注意が必要です。
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}`)} /> 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 にはこの概念がない。