メインコンテンツに移動

Webコンポーネントにおけるアンチパターンとは?避けたい設計・実装ミスを詳しく解説

Webコンポーネントは、特定のフレームワークへ強く依存せず、比較的長く使える UI 部品を作りやすい仕組みとして注目されてきました。Custom Elements、Shadow DOM、template といったブラウザ標準の機能を組み合わせることで、ライブラリ固有の作法に縛られすぎないコンポーネント基盤を持てる点は、大きな魅力だといえます。特に、複数のフロントエンド環境をまたいで共通部品を使いたい組織や、将来的な技術変更に耐えられる UI 資産を整えたいチームにとって、Webコンポーネントは有力な選択肢になりやすいです。

ただし、ここで最初に意識しておきたいのは、作れることと長く使いやすいことは同じではないという点です。Webコンポーネントは標準技術であるぶん、フレームワークが暗黙のうちに肩代わりしてくれる設計判断を、自分たちでかなり明示的に決めなければなりません。属性とプロパティをどう分けるのか、内部状態をどこまで持つのか、イベントをどの粒度で外へ公開するのか、Shadow DOM をどこまで閉じるのか、テーマやデザイン変更へどう対応するのかといった判断は、どれも一見小さく見えますが、実際にはコンポーネントの寿命と再利用性を大きく左右します。

本記事では、Webコンポーネントにおける代表的なアンチパターンを、単なる実装ミスの一覧としてではなく、なぜそれが問題になりやすいのか、どこで再利用性や保守性を損なうのか、そしてどういう方向へ設計を寄せるべきかという観点から整理していきます。動けばよい部品ではなく、長く育てられる部品を作るためには、成功例だけでなく失敗しやすい考え方も知っておく必要があります。アンチパターンを先に理解しておくことは、結果として設計判断の軸を持つことにもつながります。

1. Webコンポーネントでアンチパターンが起きやすい理由

Webコンポーネントでアンチパターンが生まれやすいのは、技術そのものに致命的な欠点があるからではありません。むしろ、設計上の自由度が非常に高いことが大きな理由です。フレームワークベースのコンポーネントであれば、状態管理、親子間のデータの流れ、イベント伝達、スタイルの扱いといった領域に、ある程度の流儀や暗黙のルールがあります。それに対して Webコンポーネントは、ブラウザ標準の仕組みを土台としているため、自分たちで決めなければならない範囲がかなり広くなります。

この自由さは本来強みですが、共通ルールを持たないまま使い始めると、コンポーネントごとに API の考え方や責務分担がずれやすくなります。ある部品は属性中心で設計され、別の部品はプロパティ中心で、イベント名の付け方にも一貫性がなく、スタイルの公開方法もばらばらになると、部品は一応動いていても、全体としてはかなり扱いにくい UI 基盤になります。つまり、Webコンポーネントでアンチパターンが起きやすいのは、自由度の高さそのものよりも、その自由をどう制御するかという設計ルールが不足しやすいことにあります。

1.1 自由度の高さがそのままばらつきにつながる

Webコンポーネントの大きな魅力の一つは、自分たちの事情に合わせてコンポーネント設計を柔軟に組み立てられることです。あるチームは軽量で素朴な部品群を目指すかもしれませんし、別のチームはデザインシステムとして厳密に統制された API を持つ部品群を整えたいかもしれません。このように、使い方の自由度が高いこと自体は強みですが、その反面、設計ルールを先に定めておかないと、部品ごとに考え方が少しずつずれていきやすいです。

特に問題なのは、そのばらつきが開発初期には見えにくいことです。部品数が少ないうちは、少し書き方が違うだけに見えるかもしれません。しかし、数十、数百という単位でコンポーネントが増えていくと、その違いがそのまま学習コストになります。利用側はコンポーネントを使うたびに、これは属性で渡すのか、プロパティで渡すのか、イベントはどんな名前なのか、外からテーマ変更できるのかを毎回確認しなければならなくなります。つまり、自由度の高さはメリットである一方、共通ルールがない環境では、そのまま不統一さとして現れやすいのです。

1.2 「標準技術だから安全」という誤解がある

Webコンポーネントはブラウザ標準技術であるため、どうしても標準だから長く使えそう、標準だから自然に安全そうという印象を持たれやすいです。もちろん、特定フレームワークへの依存が少なく、将来的な互換性の面で安心感があることは事実です。しかし、それはあくまで採用判断の一つの材料であり、設計が簡単になることや、自然に良い部品ができることを意味しているわけではありません。

むしろ標準技術であるぶん、フレームワークのような補助線は少なく、属性とプロパティの切り分け、カスタムイベントの意味付け、内部状態と公開状態の境界、Shadow DOM の公開範囲などを、自分たちで丁寧に決めなければなりません。ここを誤解すると、標準だからたぶん大丈夫だろうという感覚のまま設計が曖昧になり、あとから大きな負債として表れやすくなります。つまり、Webコンポーネントをうまく使うためには、標準であることに安心しすぎるのではなく、標準だからこそ設計責任がこちらへ戻ってくると理解することが大切です。

2. 属性とプロパティを混同するアンチパターン

Webコンポーネントで非常によく起こるアンチパターンの一つが、属性とプロパティの役割を曖昧にしたまま設計してしまうことです。一見すると、これは単に値の渡し方の違いに見えるかもしれません。しかし実際には、コンポーネント API の自然さ、フレームワークとの相性、データ型の扱いやすさ、デバッグしやすさ、さらには部品群全体の統一感にまで大きく影響します。最初は何となく動いていても、利用場面が増えるほど不自然さが表面化しやすい典型例です。

属性は基本的に HTML 上の文字列として表現されるものであり、プロパティは JavaScript オブジェクト上の値です。この違いは見た目以上に重要です。文字列や単純な数値、存在有無で表現できるフラグのような値であれば属性との相性は良いですが、配列、オブジェクト、コールバック、複雑な設定値のようなものを無理に属性へ押し込めると、JSON 文字列化や独自パースが必要になり、API 全体が不自然になります。つまり、属性とプロパティの使い分けは単なる実装上の都合ではなく、利用者がその部品をどれだけ自然に扱えるかを左右する問題です。

2.1 何でも属性で渡そうとする

HTML 上に全部見える形で書けたほうが分かりやすいという理由から、複雑な設定値やデータ構造まで、何でも属性で表現しようとするケースがあります。たとえば、ユーザー一覧、フィルタ条件、表示オプションの集合などを JSON 文字列にして属性で渡す設計は、一見すると宣言的で扱いやすそうに見えるかもしれません。しかし、実際には利用側で毎回 JSON.stringify() やパース処理が必要になり、型の保証も弱くなりやすいです。HTML に書けることと、自然に扱えることは必ずしも一致しません。

さらに、この設計はコンポーネント内部にも不要な責務を持ち込みます。文字列を受け取ってパースし、壊れた JSON に備え、異常系を処理し、再び内部で使いやすい構造へ戻す必要があるからです。本来であれば JavaScript の値としてそのまま受け取れば済むものを、わざわざ遠回りさせていることになります。つまり、何でも属性へ押し込める設計は、宣言的に見える一方で、利用側にも内部実装にも余計な複雑さを増やしやすいです。

2.2 属性変更とプロパティ変更の同期ルールが曖昧

属性とプロパティの両方を持たせること自体は悪くありません。問題なのは、どちらを source of truth として扱うのか、どの方向に同期するのかが曖昧なまま設計してしまうことです。属性が変わったら内部状態はどう変わるのか、プロパティが更新されたときに属性へ反映するのか、あるいはその逆なのかといったルールが見えていないと、更新の流れが非常に追いづらくなります。

この曖昧さは、特にフレームワークから利用するときに大きな問題になります。あるケースでは属性だけが変わり、別のケースではプロパティだけが変わり、結果として内部状態と表示がずれるようなことが起こると、原因の特定はかなり難しくなります。しかも、この種のずれは明確なエラーにならず、一部の条件でだけ変になるという形で現れやすいです。つまり、属性とプロパティの設計では、両方を用意すること以上に、その関係を明快にしておくことが大切です。

2.3 コード例:避けたい例と改善例

class BadUserList extends HTMLElement {  connectedCallback() {    const users = JSON.parse(this.getAttribute("users") || "[]");    this.innerHTML = users.map(user => `<li>${user.name}</li>`).join("");  } } customElements.define("bad-user-list", BadUserList);

この例では、配列データを JSON 文字列として属性から受け取る前提になっており、利用側に不要な変換負担をかけています。HTML 上に書けるという意味では一見分かりやすく見えるかもしれませんが、型の自然さは失われ、不正な文字列に対する考慮も必要になります。結果として、API としてはかなり扱いにくい設計になっています。

class GoodUserList extends HTMLElement {  set users(value) {    this._users = Array.isArray(value) ? value : [];    this.render();  }  render() {    this.innerHTML = (this._users || [])      .map(user => `<li>${user.name}</li>`)      .join("");  } } customElements.define("good-user-list", GoodUserList);

こちらは複雑なデータをプロパティで受け取る設計に寄せており、JavaScript からかなり自然に扱いやすくなっています。属性は宣言的な単純値に、プロパティは配列やオブジェクトのような複雑な値に、という基本ルールを守るだけでも API は大きく安定します。つまり、この違いは単なる書き方の差ではなく、利用者にどれだけ自然なインターフェースを提供できているかの差でもあります。

3. 内部状態を持ちすぎるアンチパターン

Webコンポーネントにおいて、内部状態を持つこと自体はまったく不自然ではありません。開閉状態、選択状態、入力途中の値、一時的な表示フラグなど、部品の中だけで完結する状態は数多くあります。むしろ、そうした内部状態を適切に持つことで、コンポーネントとしての独立性や扱いやすさが高まることもあります。問題なのは、内部状態を持つことそのものではなく、どこまでを内部で抱え、どこからを外部と共有すべきかの境界が曖昧になることです。

特に、この部品だけで全部完結させたいという発想が強くなると、部品は便利そうに見える反面、親コンポーネントや画面全体の状態管理と噛み合いにくくなります。最初は自己完結型の部品として扱いやすく見えても、実際の画面ではフォーム全体、画面遷移、他部品との同期、保存・復元のような要件が入ってきます。そのとき、内部に隠された状態がむしろ障害になることがあります。つまり、内部状態を持ちすぎるアンチパターンの本質は、便利さの追求ではなく、source of truth と責務境界を曖昧にしてしまうことにあります。

3.1 外部制御可能な状態まで内部で抱え込む

モーダルの open 状態やセレクトの現在値、アコーディオンの開閉状態のように、本来は親側が制御したいことの多い状態まで内部で強く抱え込んでしまうと、コンポーネントは急に統合しづらくなります。小規模なデモでは問題なく動いていても、実際のアプリケーションでは URL 状態との同期、フォーム送信との連携、複数部品の相互制御などが必要になり、その時点で内部状態の多さが足かせになりやすいです。

この問題がやっかいなのは、最初は便利さとして感じられることです。内部で全部面倒を見てくれる部品は、単体では扱いやすそうに見えます。しかし、利用側から見ると制御しづらく、結局は外側で整合を取るための補助コードやラッパーが増えていきます。つまり、外部から制御される可能性のある状態まで内部へ抱え込む設計は、短期的には楽でも、長期的には統合コストを大きく押し上げやすいです。

3.2 内部状態変更の通知が曖昧

内部状態を持つのであれば、その変化を外へどう返すかも同じくらい重要です。変更のたびに通知するのか、確定時だけ通知するのか、初期化時の state 変化も通知するのかといったルールが曖昧だと、利用側はかなり不安定なコードを書かざるを得ません。ある部品では細かくイベントが飛び、別の部品では最後にしか通知されないような状態では、部品群としての一貫性も失われます。

さらに、この曖昧さはデバッグやテストのしづらさにもつながります。利用側は今の状態は内部を見ればよいのか、それとも外へ出てきたイベントを信じればよいのかを判断しにくくなります。つまり、内部状態の設計は何を持つかだけで終わるものではなく、その変化をどう外部へ見せるかまで含めて設計しなければ成立しません。

3.3 避けたい考え方

この部品だけで全部完結させたいという考え方は、一見すると理想的に聞こえることがあります。部品が自己完結していれば、利用側の負担が減り、どこでもそのまま使えそうに見えるからです。しかし、実際にはその発想が強すぎると、コンポーネントは外側のアプリケーション状態から切り離されすぎてしまい、逆に扱いにくくなります。Webコンポーネントが独立していることと、アプリケーション全体から見て不透明であることは同じではありません。

本当に使いやすい部品は、内部で完結してよい状態と、外部から制御・監視できるべき状態がきちんと分かれています。必要な状態は外側と共有し、内部に閉じてよいものだけを限定して持つからこそ、部品は軽すぎず重すぎず、ちょうどよい境界を保てます。つまり、避けたいのは内部状態が多いことそのものではなく、何を内部へ閉じ、何を外部へ見せるべきかの判断を放棄してしまうことです。

4. カスタムイベント設計が曖昧なアンチパターン

Webコンポーネントでは、部品の外側とやり取りするためにカスタムイベントを使う場面が多くなります。そのため、イベント設計の品質は、そのままコンポーネント API の品質に直結します。にもかかわらず、イベントをとりあえず投げればよい通知として扱ってしまうアンチパターンは非常に多いです。イベント名、発火タイミング、payload、bubblescomposed の方針が揃っていないと、部品は一応動いていても、利用側から見るとかなり扱いづらくなります。

イベントは単なる技術的な連絡手段ではなく、そのコンポーネントが外部へ何を約束するかを表す重要な契約です。選択が変わったのか、開閉状態が変わったのか、入力途中なのか確定なのか、利用側はその意味をイベント名と payload から素早く理解できるべきです。そこが曖昧だと、利用側は毎回内部実装を推測しながら使わなければならなくなります。つまり、カスタムイベント設計のアンチパターンは、イベントの数の問題以上に、利用側へ解釈の負担を押し付けることにあります。

4.1 内部イベントをそのまま外へ出す

コンポーネントの内部では、hoverfocusmousedownkeydown など、細かなイベントが多数発生します。こうしたイベントは内部挙動を成立させるためには重要ですが、それらをそのまま外部へ露出させると、部品の内部都合が利用側へ漏れてしまいます。利用側が本当に知りたいのはホバーされたことではなく、候補が変わった、開閉状態が変わった、値が確定したといった意味のある変化であることがほとんどです。

内部イベントをそのまま公開する設計は、一見すると柔軟に見えるかもしれません。しかし実際には、利用側が内部の操作フローまで理解しなければ使えない API になりやすく、部品としての完成度は下がります。つまり、外へ出すべきなのは物理的な操作そのものではなく、その操作の結果として外部が知るべき意味です。この整理ができていないと、イベントが増えても、使いやすさはあまり増えません。

4.2 イベント名が意味ではなく操作ベース

click-item のような名前は、一見すると直感的で分かりやすそうに見えます。しかし、そのイベントが何を意味するのかを利用側へ委ねてしまうため、公開イベントとしてはあまり親切ではありません。クリックが起きたことを知らせたいのか、選択が確定したことを知らせたいのか、単に内部トリガーが動いたことを伝えたいのかが、名前だけでは分かりにくいからです。

これに対して、item-selectopen-changevalue-commit のような意味ベースの命名は、利用側がそのイベントをどう扱えばよいかを理解しやすくします。今はクリックだけで発火しているとしても、将来的にキーボードやタッチなど別の入力手段で同じ状態変化が起こることは珍しくありません。そのとき、操作ベースの名前では API の意味がずれてしまいます。つまり、イベント名は単なるラベルではなく、部品が外部へ公開する意味を整理するための設計要素です。

4.3 コード例:イベント設計の悪い例と良い例

this.dispatchEvent(new CustomEvent("click-item", {  detail: { index: 2 } }));

この例では、イベント名が物理操作ベースであり、返される情報も index だけなので、利用側はその意味を自分で補完しなければなりません。どの項目が選ばれたのかを知るには、外側で別の配列や対応表を参照する必要が出てくるかもしれません。イベント自体は発火していても、API としてはまだ利用側へ仕事を残している状態です。

this.dispatchEvent(new CustomEvent("item-select", {  bubbles: true,  composed: true,  detail: {    id: item.id,    value: item.value,    label: item.label  } }));

こちらは意味ベースの命名になっており、利用側が次の処理へ進むために必要な情報も detail に整理されています。さらに、bubblescomposed の設定により、イベントがどの範囲まで届くかも見えやすくなっています。つまり、良いイベント設計とは単にイベントを飛ばすことではなく、利用側がそのイベントをどう受け取り、どう使うかまで見据えて設計することだと分かります。

5. Shadow DOMを過信するアンチパターン

Shadow DOM は Webコンポーネントを象徴する機能の一つであり、スタイル衝突を防ぎ、内部構造をある程度カプセル化できるという大きな利点があります。大規模な UI では、親側のスタイルが意図せず流れ込んだり、クラス名の衝突が起きたりすることがよくあるため、このカプセル化は非常に強力です。そのため、Webコンポーネントを採用する理由として Shadow DOM を挙げるケースも多くあります。

ただし、この強さゆえに起きやすいアンチパターンがあります。それが、Shadow DOM を使えば自動的に良いコンポーネントになると考えてしまうことです。実際には、どこまでを閉じ、どこを外へ開くのかを設計していないと、かえって調整しづらく、テーマ変更しづらく、再利用しにくい部品になりやすいです。Shadow DOM の価値は、単に閉じられることではなく、必要なものだけを安全に開けることにもあります。

5.1 完全に閉じてテーマ変更不能にする

Shadow DOM を使うと、内部のスタイルや構造を親側からある程度切り離せるため、つい全部閉じたほうが安全だと考えがちです。短期的にはそのほうが安定して見えることもありますが、共通部品として長く使うことを考えると、色、余白、サイズ、密度、タイポグラフィといった調整ポイントを一切外から変えられない設計はかなり不便です。ブランド差分、ダークモード、アクセシビリティ向上のための調整など、見た目を変えたい要件は実務ではかなり高い確率で発生します。

問題なのは、完全に閉じることそのものではなく、何を固定し、何を外から調整可能にするかという設計判断を放棄してしまうことです。その結果、親側では特殊対応や無理な上書きが必要になり、Shadow DOM を使った意味が薄れてしまいます。つまり、必要なのは全面的な遮断ではなく、どこを守り、どこを安全に開くかを設計することです。

5.2 外部から内部構造へ依存させる

一方で、Shadow DOM を使っているにもかかわらず、外部コードが内部要素のクラス名や DOM 構造を前提にしてしまうのも、大きなアンチパターンです。内部のボタン構造やラッパー階層を外から当然のように期待している時点で、カプセル化の利点を自分で壊していることになります。内部実装を少し変えただけで外部コードが壊れるなら、その部品は見た目だけ Shadow DOM を使っているにすぎません。

この問題の背景には、外から調整したいのに正式な公開手段がないという事情もあります。本来なら CSS カスタムプロパティや ::part などで公開すべきものを公開していないため、利用側がやむを得ず内部構造へ依存してしまうのです。したがって、外部から内部へ依存させないためには、単に内部を触らないでくださいと言うだけでなく、必要な調整口を正式な API として設計しておくことが欠かせません。

5.3 推奨される方向性

Shadow DOM をうまく使うためには、内部を全部閉じるか、全部開くかという二択で考えないことが大切です。実務では、内部構造や詳細な DOM は守りつつ、色、余白、パーツ単位の装飾など、外部から調整したいポイントだけを CSS カスタムプロパティや ::part、あるいは必要最低限の属性やプロパティとして公開するほうが、バランスのよい設計になります。つまり、Shadow DOM は遮断のための機能というより、公開境界を整理するための機能と捉えたほうが実践的です。

この視点を持つと、Shadow DOM の価値は単なるカプセル化ではなく、内部実装を守りながら、外部とどう接続するかを明確にできることに見えてきます。テーマ変更やブランド差分に耐えられる部品ほど、何を外へ開くかがよく設計されています。つまり、推奨される方向性とは、Shadow DOM を過信することではなく、閉じることと開くことの両方を設計対象として扱うことです。

6. スタイルとテーマ対応を後回しにするアンチパターン

Webコンポーネントの部品を作るとき、最初はとにかく動く UI を先に作り、そのあとでテーマや見た目の調整を考えようとすることがあります。これは開発の流れとしては自然に見えるかもしれませんが、実はかなり危険です。なぜなら、スタイルの公開方法やテーマ変更の受け口は、単なる見た目の仕上げではなく、コンポーネント API の一部だからです。後から追加しようとすると、内部 DOM 構造や Shadow DOM の設計そのものを見直さなければならなくなることが少なくありません。

スタイルとテーマ対応の問題は、CSS の書き方だけに閉じた話ではありません。どの部分を変えられるのか、どの粒度でトークンを持つのか、部品ごとに公開方法を揃えるのかといったことは、そのままデザインシステム全体の運用しやすさにつながります。つまり、テーマ対応を後回しにするアンチパターンの本質は、見た目の制御方法を API として設計していないことにあります。

6.1 外から調整する前提を持たない

共通部品である以上、将来的に見た目を調整したい要望が出るのはほぼ避けられません。ブランドごとの差分、ダークモード、画面密度の調整、アクセシビリティ向上のためのコントラスト変更など、実際の現場では少しだけ見た目を変えたいという要件が何度も出てきます。それにもかかわらず、最初から外部調整を一切想定せずに作ってしまうと、後からの変更はかなり苦しくなります。

ここで重要なのは、何でも外から変えられるようにすることではありません。むしろ、変える必要がありそうなポイントだけを見極めて、安全な調整口として用意しておくことが大切です。調整前提を持たない設計は短期的には簡単ですが、後で変更要件が来たときに、内部構造への無理な依存や複雑な上書きルールを生みやすいです。つまり、外から調整する前提を持つとは、柔軟性を増やすこと以上に、将来の変更を安全に受け止める準備をしておくことです。

6.2 デザイントークンを持たずに作り始める

色、余白、フォントサイズ、ボーダー半径といった値を、その場の感覚で直接 CSS に書き込んでいくと、初期段階では素早く作れているように見えるかもしれません。しかし、部品が増えたあとでテーマ統一やブランド差分対応をしようとすると、どこに何の値が散らばっているのかを追うだけで大きな手間になります。値そのものではなく、この色は何のための色か、この余白は何の意味を持つかという意味で整理されたデザイントークンを持っていないと、UI 基盤はかなり育てにくくなります。

Webコンポーネントでもこの事情は同じです。むしろ Shadow DOM と組み合わせると、外部からの見通しが減るぶん、内部で使う値の意味付けがより重要になります。トークンベースで設計されていれば、テーマ変更のときも部品全体を壊さずに対応しやすくなりますし、複数部品での見た目の一貫性も保ちやすいです。つまり、デザイントークンを持たずに作り始めることは、単なる CSS 管理の問題ではなく、長期運用での変更耐性を自ら下げてしまうことに近いです。

7. ライフサイクルと副作用の扱いが雑なアンチパターン

Webコンポーネントには connectedCallback()disconnectedCallback() といったライフサイクルフックがあり、ここで初期化、イベント登録、observer の開始、外部との接続などを行うことが多いです。これ自体は自然な使い方ですが、このライフサイクルを雑に扱うと、メモリリーク、イベントの重複登録、不要な再初期化、ちらつき、意図しない副作用といった問題が起きやすくなります。特にコンポーネントの再接続や再配置が発生する環境では、その影響がかなり見えやすくなります。

ライフサイクル由来の問題は、開発初期には見えにくいのも厄介です。一度表示して終わるような簡単な画面では問題が表面化しなくても、実際のアプリケーションでは部品が何度もマウント・アンマウントされたり、外部との接続を繰り返したりします。つまり、ライフサイクル設計は最初に動くかどうかではなく、繰り返し使われたときにも破綻しないかどうかで見るべき領域です。

7.1 登録と解除をセットで考えない

イベントリスナー、ResizeObserver、MutationObserver、タイマー、外部 subscription などは、始めたら終わらせるまで責任を持つ必要があります。それにもかかわらず、connectedCallback() に登録処理だけを書いて、disconnectedCallback() 側の解除を十分に考えていない実装はかなり多いです。この設計だと、コンポーネントが DOM から外れたあとも見えないところで処理が動き続け、メモリリークや不要なイベント発火の原因になります。

しかも、この種の問題はすぐ壊れる形ではなく、たまに重くなる、同じイベントが何回も飛ぶ、画面を行き来すると挙動が変になるといった、追いにくい症状として現れやすいです。だからこそ、ライフサイクルは初期化の入口であると同時に、クリーンアップの出口としても同じ重みで扱うべきです。つまり、登録と解除をセットで考えないことは、単なる書き漏れではなく、副作用の寿命を設計していないことに等しいです。

7.2 毎回フル初期化してしまう

再接続や属性変更のたびにコンポーネント全体をまるごと作り直す設計は、最初は分かりやすく見えることがあります。状態管理が楽で、毎回ゼロから組み直せば整合も取りやすいように感じるからです。しかし、部品の数が増え、入れ子が深くなり、更新頻度が高くなるほど、この単純さはそのまま負荷へ変わっていきます。小さな部品では目立たなくても、実際の画面ではパフォーマンス低下やちらつきとして現れやすいです。

本来であれば、初回だけ行う処理、属性変更時に見直す処理、差分だけ更新すればよい処理を分けて考えるほうが安定します。毎回フル初期化する設計は、ライフサイクルの整理を放棄しているぶん、短期的には簡単でも長期的にはかなり苦しくなります。つまり、毎回ゼロから作り直すことが問題なのではなく、何を再利用し、何を更新し、何を破棄するのかの判断がないことが問題です。

7.3 コード例:危険な例と改善例

connectedCallback() {  window.addEventListener("resize", this.handleResize); }

この例では、connectedCallback() のたびに resize リスナーを登録していますが、解除処理が用意されていません。最初は問題なく見えるかもしれませんが、コンポーネントが再接続される環境では、そのたびに同じリスナーが積み上がっていく危険があります。短くて分かりやすいコードではありますが、ライフサイクル全体を見たときにはかなり不安定です。

connectedCallback() {  if (!this._boundResize) {    this._boundResize = this.handleResize.bind(this);  }  window.addEventListener("resize", this._boundResize); } disconnectedCallback() {  window.removeEventListener("resize", this._boundResize); }

こちらは登録と解除をセットで持ち、さらに bind 済み関数を再利用することで、再接続時も比較的安全に扱えるようになっています。良い例と悪い例の差は、単に数行多いか少ないかではなく、副作用をいつ始め、いつ終わらせるかまで設計できているかの差です。ライフサイクル設計において重要なのは、動き始めることだけではなく、必要なときだけ動き、不要になったらきちんと止まることです。

8. Webコンポーネントを「何でもできる箱」にしてしまうアンチパターン

最後に挙げたいのは、Webコンポーネントを過信して、あらゆる責務を一つの部品へ詰め込んでしまうアンチパターンです。UI 表示、状態管理、データ取得、入力検証、分析ログ、ページ遷移、グローバルストア連携まで一つのコンポーネントに入れてしまうと、最初は全部入りで便利な部品に見えることがあります。確かに短期間で機能をまとめたい場面では魅力的に映るかもしれません。しかし、その便利さはたいてい長続きしません。利用場所が増えたり、仕様差分が出たり、別画面へ流用したりした瞬間に、再利用よりも調整コストのほうが大きくなりやすいからです。

Webコンポーネントの本来の強みは、ブラウザ標準ベースで独立性の高い UI 部品を作れることです。つまり、責務を小さく保ち、意味のある境界で再利用しやすい単位へ切り出すことに向いています。それにもかかわらず、何でも一つの箱へ詰め込んでしまうと、その独立性はむしろ失われます。部品が便利な箱に見えるほど、実際にはアプリケーション固有の事情に強く縛られ、どこでも使える部品ではなく、この画面専用の重い塊になりやすいです。

8.1 UI とアプリケーションロジックを混ぜすぎる

データ取得、ページ遷移、グローバルストア操作、分析イベント送信など、本来はアプリケーション層が持つべき責務まで UI 部品の内部へ入れてしまうと、そのコンポーネントは急速に再利用しにくくなります。特定画面では便利に見えても、別の画面や別の文脈へ持っていった瞬間に、この依存も必要、この前提も必要と条件が増え、汎用部品とは呼びづらくなります。UI とアプリケーションロジックの境界が曖昧な部品は、見た目以上に寿命が短いです。

本来、UI 部品は表示と入力の意味を整理し、必要な状態やイベントを適切に外へ見せることで価値を持ちます。そこへアプリケーション固有のロジックが大量に入り込むと、部品としての境界がぼやけ、責務が一気に重くなります。つまり、UI とアプリケーションロジックを混ぜすぎることの問題は、コード量が増えること以上に、部品としての独立性が失われることにあります。

8.2 「便利さ」を優先しすぎて API が巨大化する

あらゆる利用ケースを一つの部品で吸収しようとすると、属性、プロパティ、イベント、特殊オプション、例外条件がどんどん増えていきます。その結果、見た目には柔軟そうでも、実際には誰も全体を理解しきれない巨大 API が生まれやすいです。利用側は必要な機能だけを使っているつもりでも、内部では多数の分岐や特殊ケースが存在し、変更時の影響範囲も大きくなります。柔軟さを求めたはずが、いつの間にか保守しづらい部品になっているというのは非常によくある流れです。

本当に扱いやすい部品は、全部入りであることよりも、責務がはっきりしていて、何をしてくれる部品なのかが素直に分かることのほうが重要です。大きな部品を一つ作るより、意味のある単位で小さく切り分けた部品を組み合わせるほうが、長期的には理解しやすく、変更にも強くなります。つまり、便利さを優先しすぎて API を巨大化させることは、機能の多さを得る代わりに、予測可能性と保守性を失うことになりやすいです。

8.3 最後に意識したいこと

Webコンポーネントのアンチパターンを避けるうえで本当に大切なのは、特定の小技やテクニックを個別に覚えることではありません。もちろん、属性とプロパティの使い分け、カスタムイベントの命名、Shadow DOM の公開範囲、ライフサイクルでのクリーンアップといった実践的な知識は重要です。しかし、それ以上に大切なのは、責務を小さく保ち、公開 API を明確にし、一貫したルールで部品を育てることです。自由度が高い技術だからこそ、その自由をどう制御するかが品質に直結します。

言い換えれば、Webコンポーネントをうまく使うとは、標準技術をそのまま使うことではなく、標準技術の上に、自分たちが守るべき設計ルールを築くことです。部品を増やす速度よりも、境界の明確さ、一貫した命名、調整口の設計、状態の責務分担といった基礎を固めることのほうが、長く見るとずっと大きな差になります。つまり、最後に意識したいのは、便利そうな部品を急いで作ることではなく、長く壊れにくい部品の条件を最初から設計へ織り込むことです。

おわりに

Webコンポーネントにおけるアンチパターンは、単にコードが汚い、書き方が少しよくないといった表面的な話ではありません。属性とプロパティの混同、内部状態の持ちすぎ、曖昧なイベント設計、Shadow DOM の過信、テーマ対応の後回し、ライフサイクル管理の雑さ、責務の詰め込みといった問題は、どれも最初は小さく見えて、部品数が増えたときやチーム開発が広がったときに大きな負債として表れやすいものです。つまり、アンチパターンを知ることは、単に失敗を避けるためだけでなく、長く使える部品の条件を逆算して理解することにもつながります。

Webコンポーネントは、標準技術だから自然にうまくいくわけではありません。むしろ、フレームワークの流儀に守られにくいぶん、属性、イベント、状態、スタイル、ライフサイクル、公開境界といった設計責任がこちらへ強く戻ってきます。その責任をきちんと引き受けられれば、長寿命で再利用性の高い UI 基盤を作ることができますし、逆に曖昧なまま進めると、使えるはずの技術が扱いにくい部品群へ変わっていきます。だからこそ、Webコンポーネントを採用するときは、何ができるかだけでなく、何をすると壊れやすくなるかまで含めて理解しておくことが大切です。

LINE Chat