React 開発者が知るべき Web Components の基本概念と実装方法

React を使った開発に慣れ親しんでいる皆さんは、おそらく「コンポーネント」という概念に深い愛着をお持ちでしょう。再利用可能で保守性の高いコンポーネントを作ることの素晴らしさを、日々実感されているのではないでしょうか。
そんな中で最近注目を集めているのが「Web Components」という技術です。これは React のようなフレームワークに依存することなく、ブラウザ標準の技術だけでコンポーネントを作成できる仕組みなのです。
React 開発者の方にとって、Web Components は新しい選択肢を提供してくれる可能性があります。フレームワークの枠を超えて再利用できるコンポーネントを作れたら、どんなに素敵でしょうか。
この記事では、React の知識を活かしながら Web Components を理解し、実際に活用するための方法を詳しくご紹介します。きっと新しい技術への扉が開かれることでしょう。
背景
React の普及とコンポーネント指向開発の浸透
React の登場以来、Web 開発の世界ではコンポーネント指向の開発手法が当たり前のものとなりました。UI を小さな部品に分割し、それらを組み合わせてアプリケーションを構築するという考え方は、もはや開発者にとって欠かせないものです。
React を使っていると、以下のようなメリットを日常的に感じられているのではないでしょうか。
# | メリット | 具体的な効果 |
---|---|---|
1 | 再利用性 | 同じコンポーネントを複数の場所で使える |
2 | 保守性 | 変更箇所が明確で修正が容易 |
3 | テスト容易性 | 単体でテストしやすい構造 |
4 | 開発効率 | チーム開発での分担がしやすい |
これらの恩恵を受けながら、私たちは素晴らしいユーザーエクスペリエンスを提供できるアプリケーションを作り続けてきました。
ブラウザ標準技術としての Web Components の進化
一方で、ブラウザベンダーたちも同様の課題に取り組んでいました。Web の標準技術として、フレームワークに依存しないコンポーネント技術を提供したいという想いがあったのです。
その結果生まれたのが Web Components です。これは以下の標準技術の組み合わせによって実現されています。
- Custom Elements: 独自のHTML要素を定義できる技術
- Shadow DOM: カプセル化されたDOM構造を作る技術
- HTML Templates: 再利用可能なHTMLの雛形を定義する技術
これらの技術は、すでに主要なブラウザでサポートされており、実用レベルに達しています。Internet Explorer のサポート終了に伴い、モダンブラウザでの対応状況は非常に良好です。
フレームワークに依存しない再利用可能なコンポーネントの需要
企業の開発現場では、複数のプロジェクトで異なるフレームワークを使うケースが増えています。例えば、以下のような状況はよくある話でしょう。
- メインのアプリケーションは React で構築
- 管理画面は Vue.js で開発
- ランディングページは静的サイトジェネレーター使用
- レガシーシステムはjQueryで動作
このような環境で、統一されたUIコンポーネントを使いたいというニーズが高まっています。デザインシステムの構築や、ブランドの一貫性を保つために、フレームワークを跨いで利用できるコンポーネントが求められているのです。
課題
React 開発者が Web Components を理解する必要性
React 開発者の皆さんが Web Components を学ぶべき理由は、単に新しい技術を習得するということ以上の意味があります。
技術選択の幅が広がることで、プロジェクトの要件に応じてより適切な判断ができるようになるでしょう。また、Web標準への理解を深めることで、より本質的なWeb開発のスキルが身につきます。
さらに、キャリアの観点からも重要です。フレームワークのトレンドは移り変わりますが、Web標準の知識は長期的に価値を持ち続けるのです。
フレームワーク間での技術選択の迷い
多くの開発者が直面する課題として、「いつReactを使い、いつWeb Componentsを使うべきか」という判断の難しさがあります。
以下のような疑問を持たれたことはありませんか?
- 小さなウィジェットを作る場合、Reactは大げさすぎるのではないか
- 他のチームが作ったコンポーネントを自分のReactアプリで使いたい
- 将来的にフレームワークを変更する可能性があるプロジェクトではどうすべきか
これらの疑問に適切に答えるためには、両方の技術の特性を深く理解する必要があります。
既存の React プロジェクトとの共存方法の不明確さ
実際のプロジェクトでは、いきなり全てを Web Components に置き換えることはできません。段階的な移行や、部分的な導入を考える必要があります。
しかし、以下のような技術的な課題があります。
# | 課題 | 詳細 |
---|---|---|
1 | イベント処理 | React のイベントシステムとの連携方法 |
2 | 状態管理 | React の state と Web Components の連携 |
3 | スタイリング | CSS-in-JSとの共存方法 |
4 | TypeScript対応 | 型安全性の確保 |
これらの課題を解決する具体的な方法を知ることで、安心してWeb Componentsを導入できるようになります。
解決策
Web Components の3つの主要技術
Web Components を理解するためには、まずその構成要素である3つの技術について詳しく見ていきましょう。React 開発者の皆さんにとって馴染みのある概念と比較しながら説明いたします。
Custom Elements
Custom Elements は、独自のHTML要素を定義できる技術です。React でコンポーネントを定義するのと似ていますが、HTMLの標準要素として登録される点が大きな違いです。
Custom Elementsの基本的な定義方法をご紹介します。
javascript// カスタム要素のクラスを定義
class MyButton extends HTMLElement {
constructor() {
super();
this.innerHTML = `
<button class="my-button">
クリックしてください
</button>
`;
}
}
// カスタム要素を登録
customElements.define('my-button', MyButton);
この例では、<my-button></my-button>
という新しいHTML要素を作成しています。React の関数コンポーネントと似たような感覚で使えますね。
Shadow DOM
Shadow DOM は、コンポーネントの内部構造を外部から隔離する技術です。React の場合、CSSの名前衝突を避けるために CSS Modules や styled-components を使いますが、Shadow DOM では標準技術としてカプセル化が提供されます。
Shadow DOM を使った例をご覧ください。
javascriptclass IsolatedButton extends HTMLElement {
constructor() {
super();
// Shadow DOM を作成
const shadow = this.attachShadow({ mode: 'open' });
// スタイルも含めて定義
shadow.innerHTML = `
<style>
.button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
.button:hover {
background-color: #0056b3;
}
</style>
<button class="button">
<slot></slot>
</button>
`;
}
}
Shadow DOM内のCSSは外部に影響を与えず、逆に外部のCSSも影響を受けません。これにより、真の意味でのコンポーネントの独立性が保たれます。
HTML Templates
HTML Templates は、再利用可能なHTMLマークアップのテンプレートを定義する技術です。React の JSX に似た役割を果たしますが、純粋なHTML として記述されます。
html<!-- HTML Template の定義 -->
<template id="card-template">
<style>
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
margin: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
}
</style>
<div class="card">
<div class="card-title"></div>
<div class="card-content">
<slot></slot>
</div>
</div>
</template>
このテンプレートをJavaScriptから利用する方法です。
javascriptclass CardComponent extends HTMLElement {
constructor() {
super();
const template = document.getElementById('card-template');
const templateContent = template.content;
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(templateContent.cloneNode(true));
// タイトルを設定
const title = this.getAttribute('title') || 'カードタイトル';
shadowRoot.querySelector('.card-title').textContent = title;
}
}
customElements.define('card-component', CardComponent);
React との比較による理解
Web Components と React を比較することで、それぞれの特性と適用場面がより明確になります。技術的な違いを詳しく見てみましょう。
コンポーネント設計思想の共通点と相違点
両方の技術とも、UI を独立したコンポーネントに分割するという基本思想は共通しています。しかし、アプローチには重要な違いがあります。
# | 観点 | React | Web Components |
---|---|---|---|
1 | 定義方法 | JSX + JavaScript | HTML + JavaScript |
2 | カプセル化 | ツールチェーンに依存 | ブラウザ標準機能 |
3 | 再利用性 | React エコシステム内 | フレームワーク非依存 |
4 | 学習コスト | 中程度(JSX、hooks等) | 高め(DOM API理解必要) |
5 | 開発体験 | 非常に良い | 改善中 |
React の場合、JSX によって宣言的にUIを記述できる素晴らしい体験があります。一方、Web Components では、より低レベルなDOM操作が必要になることが多いです。
ライフサイクルの違い
React のライフサイクルメソッドや hooks に慣れている方にとって、Web Components のライフサイクルは少し異なる概念となります。
React のライフサイクル例です。
javascript// React 関数コンポーネントでのライフサイクル
function ReactButton({ onClick, children }) {
useEffect(() => {
// マウント時の処理
console.log('コンポーネントがマウントされました');
return () => {
// アンマウント時の処理
console.log('コンポーネントがアンマウントされます');
};
}, []);
return (
<button onClick={onClick}>
{children}
</button>
);
}
Web Components での対応するライフサイクル実装です。
javascriptclass WebComponentButton extends HTMLElement {
connectedCallback() {
// DOM に追加された時(React の useEffect に相当)
console.log('コンポーネントがDOMに追加されました');
this.render();
}
disconnectedCallback() {
// DOM から削除された時(React のクリーンアップに相当)
console.log('コンポーネントがDOMから削除されました');
}
attributeChangedCallback(name, oldValue, newValue) {
// 属性が変更された時(React のprops変更に相当)
console.log(`属性 ${name} が ${oldValue} から ${newValue} に変更されました`);
this.render();
}
static get observedAttributes() {
return ['text', 'color'];
}
render() {
this.innerHTML = `
<button style="color: ${this.getAttribute('color') || 'black'}">
${this.getAttribute('text') || 'ボタン'}
</button>
`;
}
}
状態管理の考え方
React では useState
や useReducer
を使って状態管理を行いますが、Web Components では異なるアプローチが必要です。
React での状態管理例です。
javascriptfunction Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<div>
<p>カウント: {count}</p>
<button onClick={increment}>+1</button>
</div>
);
}
Web Components での状態管理実装です。
javascriptclass CounterComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._count = 0; // プライベートな状態
}
get count() {
return this._count;
}
set count(value) {
this._count = value;
this.render(); // 状態変更時に再レンダリング
}
connectedCallback() {
this.render();
this.addEventListener('click', this.handleClick.bind(this));
}
handleClick(event) {
if (event.target.matches('button')) {
this.count++;
}
}
render() {
this.shadowRoot.innerHTML = `
<div>
<p>カウント: ${this.count}</p>
<button>+1</button>
</div>
`;
}
}
このように、Web Components では明示的に再レンダリングのタイミングを管理する必要があります。React の自動的な再レンダリングの便利さを実感される方も多いでしょう。
具体例
実際にコードを書きながら、Web Components の実装方法を学んでいきましょう。React 開発者の皆さんが日常的に使っているパターンを Web Components で再現することで、理解が深まるはずです。
シンプルなカスタム要素の作成
まずは、最もシンプルなカスタム要素から始めましょう。React でよく作るボタンコンポーネントを Web Components で実装してみます。
基本的なカスタムボタンの実装
javascriptclass SimpleButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
button {
background-color: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s ease;
}
button:hover {
background-color: #0056b3;
}
button:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
</style>
<button>
<slot></slot>
</button>
`;
}
}
// カスタム要素を登録
customElements.define('simple-button', SimpleButton);
HTML での使用方法です。
html<!-- 基本的な使用 -->
<simple-button>クリックしてください</simple-button>
<!-- スロットを使った複雑なコンテンツ -->
<simple-button>
<span>📧</span> メールを送信
</simple-button>
属性を受け取るボタンコンポーネント
React の props のように、属性を受け取れるようにしてみましょう。
javascriptclass AttributeButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
this.addEventListeners();
}
static get observedAttributes() {
return ['variant', 'size', 'disabled'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
get variant() {
return this.getAttribute('variant') || 'primary';
}
get size() {
return this.getAttribute('size') || 'medium';
}
get disabled() {
return this.hasAttribute('disabled');
}
getVariantStyles() {
const variants = {
primary: 'background-color: #007bff; color: white;',
secondary: 'background-color: #6c757d; color: white;',
success: 'background-color: #28a745; color: white;',
danger: 'background-color: #dc3545; color: white;'
};
return variants[this.variant] || variants.primary;
}
getSizeStyles() {
const sizes = {
small: 'padding: 8px 16px; font-size: 12px;',
medium: 'padding: 12px 24px; font-size: 14px;',
large: 'padding: 16px 32px; font-size: 16px;'
};
return sizes[this.size] || sizes.medium;
}
続きのレンダリング部分とイベント処理です。
javascript render() {
const variantStyles = this.getVariantStyles();
const sizeStyles = this.getSizeStyles();
this.shadowRoot.innerHTML = `
<style>
button {
${variantStyles}
${sizeStyles}
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
</style>
<button ${this.disabled ? 'disabled' : ''}>
<slot></slot>
</button>
`;
}
addEventListeners() {
const button = this.shadowRoot.querySelector('button');
button.addEventListener('click', (event) => {
if (!this.disabled) {
// カスタムイベントを発火
this.dispatchEvent(new CustomEvent('button-click', {
detail: {
variant: this.variant,
size: this.size
},
bubbles: true
}));
}
});
}
}
customElements.define('attribute-button', AttributeButton);
HTML での使用例です。
html<!-- 様々な属性を持つボタン -->
<attribute-button variant="success" size="large">
保存する
</attribute-button>
<attribute-button variant="danger" size="small" disabled>
削除する
</attribute-button>
<script>
// カスタムイベントのリスニング
document.addEventListener('button-click', (event) => {
console.log('ボタンがクリックされました:', event.detail);
});
</script>
React コンポーネントを Web Component に変換
実際の開発では、既存の React コンポーネントを Web Component に変換したいケースがよくあります。具体的な手順を見てみましょう。
React のカードコンポーネント
まず、変換対象となる React コンポーネントです。
javascript// React でのカードコンポーネント
function Card({ title, description, imageUrl, tags = [] }) {
return (
<div className="card">
{imageUrl && <img src={imageUrl} alt={title} className="card-image" />}
<div className="card-content">
<h3 className="card-title">{title}</h3>
<p className="card-description">{description}</p>
{tags.length > 0 && (
<div className="card-tags">
{tags.map((tag, index) => (
<span key={index} className="tag">{tag}</span>
))}
</div>
)}
</div>
</div>
);
}
Web Component への変換
同じ機能を持つ Web Component 版です。
javascriptclass CardComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
static get observedAttributes() {
return ['title', 'description', 'image-url', 'tags'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
get title() {
return this.getAttribute('title') || '';
}
get description() {
return this.getAttribute('description') || '';
}
get imageUrl() {
return this.getAttribute('image-url');
}
get tags() {
const tagsAttr = this.getAttribute('tags');
return tagsAttr ? tagsAttr.split(',').map(tag => tag.trim()) : [];
}
レンダリング部分です。
javascript render() {
this.shadowRoot.innerHTML = `
<style>
.card {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
background: white;
transition: transform 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
.card-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.card-content {
padding: 16px;
}
.card-title {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.card-description {
margin: 0 0 12px 0;
color: #666;
line-height: 1.5;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tag {
background-color: #f0f0f0;
color: #666;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
</style>
<div class="card">
${this.imageUrl ? `<img src="${this.imageUrl}" alt="${this.title}" class="card-image" />` : ''}
<div class="card-content">
<h3 class="card-title">${this.title}</h3>
<p class="card-description">${this.description}</p>
${this.tags.length > 0 ? `
<div class="card-tags">
${this.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
` : ''}
</div>
</div>
`;
}
}
customElements.define('card-component', CardComponent);
HTML での使用方法です。
html<!-- Web Component として使用 -->
<card-component
title="Web Components の基礎"
description="React 開発者向けの Web Components 入門記事です。基本的な概念から実装方法まで詳しく解説しています。"
image-url="https://example.com/image.jpg"
tags="React, Web Components, JavaScript">
</card-component>
React アプリケーション内での Web Components 利用
React アプリケーションから Web Components を使う方法も見てみましょう。
TypeScript での型定義
まず、TypeScript で Web Components を使うための型定義を追加します。
typescript// types/web-components.d.ts
declare namespace JSX {
interface IntrinsicElements {
'simple-button': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
variant?: 'primary' | 'secondary' | 'success' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
};
'card-component': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
title?: string;
description?: string;
'image-url'?: string;
tags?: string;
};
}
}
React コンポーネント内での使用
Web Components を React コンポーネント内で使用する例です。
typescriptimport React, { useEffect, useRef } from 'react';
// Web Components のインポート(副作用のために必要)
import './components/SimpleButton';
import './components/CardComponent';
const App: React.FC = () => {
const buttonRef = useRef<HTMLElement>(null);
useEffect(() => {
// Web Components のカスタムイベントをリスニング
const handleButtonClick = (event: CustomEvent) => {
console.log('Web Component からのイベント:', event.detail);
};
const button = buttonRef.current;
if (button) {
button.addEventListener('button-click', handleButtonClick as EventListener);
return () => {
button.removeEventListener('button-click', handleButtonClick as EventListener);
};
}
}, []);
return (
<div className="app">
<h1>React と Web Components の連携</h1>
{/* Web Components の使用 */}
<simple-button
ref={buttonRef}
variant="primary"
size="large"
>
Web Component ボタン
</simple-button>
<card-component
title="サンプルカード"
description="React アプリ内で Web Component を使用した例です。"
tags="React, Web Components, TypeScript"
/>
</div>
);
};
export default App;
React と Web Components 間でのデータのやり取り
より複雑なデータのやり取りが必要な場合の実装例です。
typescriptimport React, { useState, useEffect, useRef } from 'react';
interface User {
id: number;
name: string;
email: string;
}
const UserList: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const userListRef = useRef<HTMLElement>(null);
useEffect(() => {
// ユーザーデータを取得
fetchUsers().then(setUsers);
}, []);
useEffect(() => {
// Web Component にデータを渡す
const userListElement = userListRef.current;
if (userListElement && users.length > 0) {
// プロパティとして直接設定
(userListElement as any).users = users;
}
}, [users]);
const handleUserSelect = (event: CustomEvent<{ userId: number }>) => {
const selectedUser = users.find(user => user.id === event.detail.userId);
console.log('選択されたユーザー:', selectedUser);
};
return (
<div>
<h2>ユーザー一覧</h2>
<user-list-component
ref={userListRef}
onUser-select={handleUserSelect}
/>
</div>
);
};
async function fetchUsers(): Promise<User[]> {
// API からユーザー情報を取得
const response = await fetch('/api/users');
return response.json();
}
対応する Web Component の実装です。
javascriptclass UserListComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._users = [];
}
set users(value) {
this._users = value;
this.render();
}
get users() {
return this._users;
}
connectedCallback() {
this.render();
this.addEventListeners();
}
addEventListeners() {
this.shadowRoot.addEventListener('click', (event) => {
const userItem = event.target.closest('.user-item');
if (userItem) {
const userId = parseInt(userItem.dataset.userId);
this.dispatchEvent(new CustomEvent('user-select', {
detail: { userId },
bubbles: true
}));
}
});
}
render() {
this.shadowRoot.innerHTML = `
<style>
.user-list {
list-style: none;
padding: 0;
margin: 0;
}
.user-item {
padding: 12px;
border: 1px solid #e0e0e0;
margin-bottom: 8px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.user-item:hover {
background-color: #f5f5f5;
}
.user-name {
font-weight: 600;
margin-bottom: 4px;
}
.user-email {
color: #666;
font-size: 14px;
}
</style>
<ul class="user-list">
${this.users.map(user => `
<li class="user-item" data-user-id="${user.id}">
<div class="user-name">${user.name}</div>
<div class="user-email">${user.email}</div>
</li>
`).join('')}
</ul>
`;
}
}
customElements.define('user-list-component', UserListComponent);
まとめ
この記事では、React 開発者の皆さんが Web Components を理解し、実際のプロジェクトで活用するための知識をお伝えしました。技術選択の新たな選択肢として、Web Components がどのような価値を提供してくれるかがお分かりいただけたでしょうか。
Web Components の最大の魅力は、フレームワークに依存しない標準技術であることです。React で培ったコンポーネント設計の知識を活かしながら、より汎用性の高いコンポーネントを作成できるのです。
一方で、React の優れた開発体験や豊富なエコシステムも大切な価値です。どちらか一方を選ぶのではなく、プロジェクトの要件に応じて適切に使い分けることが重要でしょう。
# | 使用場面 | 推奨技術 | 理由 |
---|---|---|---|
1 | 複雑なSPAの構築 | React | 開発体験とエコシステムの豊富さ |
2 | 汎用的なUIライブラリ | Web Components | フレームワーク非依存の再利用性 |
3 | 既存サイトへの部分的な機能追加 | Web Components | 軽量で導入しやすい |
4 | チーム間での共通コンポーネント | Web Components | 技術スタックの違いを吸収 |
今後、Web Components の開発体験はさらに向上していくでしょう。lit などのライブラリを使うことで、React に近い開発体験を得ることも可能になってきています。
技術の進歩に合わせて、私たちも柔軟に学習を続けていきたいものですね。この記事が、皆さんの技術選択の一助となれば幸いです。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来