代表的な面接トピック

フロントエンド面接:保守性の高い Custom Element のライフサイクルをどのように設計するか?

フロントエンド難しい
Offer.cc 編集チーム公開日 更新日

質問

複数のチームやフレームワークで再利用可能な Custom Element を設計してください。constructor、connectedCallback、disconnectedCallback、attributeChangedCallback をどのように使い分け、Shadow DOM、属性とプロパティの同期、重複登録、ブラウザサポートをどのように処理しますか?

出題内容とスコープ

React、Vue、またはサーバーテンプレートによってレンダリングされるホスト向けに <account-summary> カスタム要素を提供する必要があります。この要素はアカウントステータスを表示し、status 属性に反応し、パブリックイベントを発行し、ドキュメントから削除されたときに購読を解除します。キーボードや支援技術のセマンティクスを損なうことなく、スタイルをカプセル化し続ける必要があります。

まずは自律カスタム要素(autonomous custom element)を前提とします。ネイティブの button やその他の組み込み要素を拡張する要件である場合は、ブラウザサポートによって設計が変わるため、カスタマイズされた組み込み要素(customized built-in elements)として個別に評価します。面接ではフレームワークのコンポーネント構文ではなく、ライフサイクルの境界とリソースの所有権がテストされます。

面接官が評価している点

  • constructor、connectedCallback、disconnectedCallback、属性監視に明確な責任を割り振れているか。
  • 各コールバックが1度だけ実行されると仮定せず、再接続、移動、アップグレード、初期属性を適切に処理できるか。
  • Shadow DOM のカプセル化をアクセシビリティ、イベント伝播、ホストとの統合と結びつけて考えられているか。
  • 冪等な登録、互換性、クリーンアップ、テストを提案できるか。

単にコールバック名を暗唱するだけでは不十分です。優れた回答は、まずリソースの所有権を定義し、次に接続、属性、レンダリング状態がどのように相互作用するかを説明します。

最初に明確にすべき質問

  1. 要素はネイティブのセマンティック要素を拡張する必要がありますか?その場合、is 構文とブラウザサポートが選択に影響します。
  2. 入力は文字列ですか、それとも呼び出し側がオブジェクトやコールバックを渡す必要がありますか?複雑な値には明示的なプロパティまたはメソッドのコントラクトが必要です。
  3. 同一ドキュメント内での移動をまたいで状態を維持する必要がありますか?これにより、connectedMoveCallback を評価するか、状態を明示的に保持するかを判断します。
  4. イベントは Shadow DOM の境界を越える必要がありますか?イベント API を選択する前に、bubblescomposed、ペイロードについて合意します。

30秒の回答フレームワーク

「私は定義、接続、属性同期、レンダリング、クリーンアップを分離します。constructor は軽量なフィールドのみを初期化し、子ノードや外部ドキュメントには依存しません。connectedCallback は購読を確立し、冪等にレンダリングします。disconnectedCallback はリスナー、タイマー、オブザーバーを解放します。必要な属性のみを監視し、attributeChangedCallback で文字列値を正規化して、単一のレンダリングパスを通じて更新をルーティングします。Shadow DOM は実装スタイルをカプセル化し、パブリックなセマンティクス、フォーカス、イベントはテスト済みのホストコントラクトの一部として維持します。最後に、再接続、移動、初期属性、削除、アップグレード、非対応ブラウザのテストで締めくくります。」

ステップごとの分析

1. 状態とリソースの所有権を定義する

接続、購読、レンダリングの状態を別々に追跡します。constructor は外部 DOM を読み取ったりネットワーク処理を開始したりしてはなりません。ブラウザはドキュメントに接続する前に要素を構築することがあるためです。

js
class AccountSummary extends HTMLElement {
  static observedAttributes = ["status"];
  #connected = false;
  #unsubscribe = null;
  #root;

  constructor() {
    super();
    this.#root = this.attachShadow({ mode: "open" });
  }
}

2. connectedCallback の繰り返し実行を安全にする

要素は削除されてから再度挿入されることがあるため、connectedCallback は無条件に別のリスナーを登録してはなりません。接続状態を確認し、購読を確立し、レンダリングを繰り返し呼び出しても安全なようにします。

js
connectedCallback() {
  if (this.#connected) return;
  this.#connected = true;
  this.#unsubscribe = accountStore.subscribe(() => this.#render());
  this.#render();
}

3. disconnectedCallback であらゆる副作用を解放する

イベントリスナー、setIntervalResizeObserver、AbortController、ストアの購読には明示的な所有者が必要です。クリーンアップ後に参照をクリアし、その後の接続で正確に1セットの新しいリソースが作成されるようにします。

js
disconnectedCallback() {
  this.#unsubscribe?.();
  this.#unsubscribe = null;
  this.#connected = false;
}

古い移動パターンでは、切断コールバックと接続コールバックが連続してトリガーされることがあります。移動をまたいだ状態維持が重要な場合は、サポートされている環境で connectedMoveCallback を評価するか、コールバックの順序に依存するのではなく、属性や永続的な状態から再構築します。

4. 属性を監視し、プロパティを分離する

observedAttributes には同期が必要な値のみをリストする必要があります。attributeChangedCallback はパース中に初期属性に対しても実行されるため、「変更された」は必ずしもユーザーが編集したばかりであることを意味しません。初期値もその後の値も同じパスで処理し、コールバック内から同じ属性を書き戻さないようにします。

js
attributeChangedCallback(name, oldValue, newValue) {
  if (oldValue === newValue) return;
  if (name === "status") this.#renderStatus(newValue ?? "unknown");
}

文字列属性は宣言的 HTML に適しています。任意の JSON 文字列を無制限のプロトコルとして扱うのではなく、配列、オブジェクト、コールバックは文書化されたプロパティやメソッドを通じて渡します。

5. Shadow DOM とホストコントラクトを設計する

Shadow DOM は内部の CSS と DOM クエリを分離しますが、セマンティクスを自動的に提供するわけではありません。正しい role、アクセシブルな名前、フォーカス順序、可視性を使用します。ホストがレイアウトに関与する必要がある場合は、:host、スロット、CSS カスタムプロパティを通じて狭いコントラクトを公開します。

css
:host { display: block; }
:host([hidden]) { display: none; }

ホストが監視する必要があるイベントについては、bubblescomposed を意図的に設定し、ペイロードを安定に保ちます。境界を越えるために内部ノードを公開する必要はありません。

6. 登録、アップグレード、互換性を処理する

名前はグローバルレジストリに1度しか定義できません。共有ライブラリでは名前空間プレフィックスを使用し、定義前に customElements.get(name) を確認する必要があります。例外をキャッチして衝突を隠蔽してはなりません。登録前にパースされた要素は定義後にアップグレードされるため、実装はその順序をサポートする必要があります。

js
const name = "account-summary";
if (!customElements.get(name)) {
  customElements.define(name, AccountSummary);
}

自律要素は通常、ブラウザ間でのデプロイが容易です。MDN にはカスタマイズされた組み込み要素に関する Safari の制限事項が記載されています。ネイティブ要素の拡張が必須である場合は、対象ブラウザのマトリックスを確認し、フォールバックを用意します。

質の高い模範解答

私ならまず所有権テーブルを作成します。constructor は Shadow Root とデフォルトフィールドを作成し、connectedCallback は購読とレンダリングを行い、disconnectedCallback は購読を解除し、attributeChangedCallback は監視対象の宣言的設定のみを処理します。各フェーズは繰り返し可能であるか安全にスキップできるようにし、「1回だけ実行される」ことをライフサイクルの保証として扱いません。

自律カスタム要素を選択し、customElements.get で登録をガードします。Shadow DOM には実装スタイルが含まれますが、コンポーネントはアクセシブルな名前、フォーカスの挙動、イベントコントラクトを公開し続けます。境界を越えるイベントには、制限されたペイロードとともに明示的な bubbles および composed 設定を使用します。属性にはプリミティブな宣言的設定を保持させ、複雑なオブジェクトにはプロパティやメソッドを使用します。

検証では、定義前パースのアップグレード、初期属性、再接続、移動、クリーンアップ、境界越えイベント、キーボード操作、スクリーンリーダーのセマンティクスをカバーします。カスタマイズされた組み込み要素が必要な場合は、まずブラウザマトリックスを検証し、互換性のコストが許容できる場合にのみそのアプローチを選択します。

よくある間違いと改善策

  • 間違い → constructor 内で子ノードを読み取ったりリクエストを開始したりする → 失敗する理由 → 要素がまだ接続されていない可能性がある → 改善策 → ドキュメントやネットワークの処理を connectedCallback に移し、リクエストを AbortController にバインドする。
  • 間違い → connectedCallback ごとにリスナーを追加する → 失敗する理由 → 再接続時に処理が重複し、メモリリークが発生する → 改善策 → 購読ハンドルを保持し、セットアップとクリーンアップを冪等なペアにする。
  • 間違い → 属性コールバックから無条件に setAttribute を呼び出す → 失敗する理由 → コールバックの無限ループを引き起こす可能性があり、初期属性のパース時にもコールバックが呼び出される → 改善策 → 新旧の値を比較し、1度だけ正規化し、同じ属性への書き戻しを避ける。
  • 間違い → Shadow DOM をアクセシビリティの解決策として扱う → 失敗する理由 → カプセル化によって名前、フォーカス、セマンティクスが作成されるわけではない → 改善策 → キーボードや支援技術のフローを用いてパブリックコントラクトをテストする。

フォローアップの質問と回答

要素が定義される前にパースされた場合、どのようにフラッシュ(ちらつき)を防ぎますか?

未定義の要素をアップグレード可能な状態として扱い、読み取り可能なフォールバックコンテンツまたはローディング状態を提供します。ブラウザは登録後にインスタンスをアップグレードします。コードが待機する必要がある場合、ホストは customElements.whenDefined("account-summary") を await できますが、ページ全体がコンポーネントスクリプトでブロックされるべきではありません。

複雑なオブジェクトを属性に配置すべきですか?

通常は配置すべきではありません。属性は文字列であり、シリアライズ可能な宣言的設定に適しています。オブジェクトやコールバックは、接続前後の挙動が明確に文書化されたプロパティまたはメソッドを使用すべきです。シリアライズが必要な場合は、バージョン、サイズ、パースエラーの処理を規定します。

Shadow DOM 内のボタンからのクリックをホストが受信できないのはなぜですか?

境界を越える伝播は composed に依存し、上位へのバブリングは bubbles に依存します。両方のオプションを明示的に選択して、安定したセマンティックイベントを発行します。境界の外側では、ターゲットが shadow host にリターゲットされることがあるため、ホストは内部ノードに依存すべきではありません。

再接続でリークが発生しないことをどのように証明しますか?

同じインスタンスの挿入と削除を繰り返し、購読数、イベント配信、タイマーハンドルをカウントした後、メモリスナップショットを使用して古いノードがガベージコレクションされることを確認します。初期属性コールバック、移動、定義前パースのアップグレードをカバーします。初回レンダリングだけではほとんど何も証明できません。

公開情報ソース

関連する質問