【ITニュース解説】Teleport a Component in Angular (and Keep Its State)
2025年09月19日に「Dev.to」が公開したITニュース「Teleport a Component in Angular (and Keep Its State)」について初心者にもわかりやすく解説しています。
ITニュース概要
Angularでコンポーネントをレイアウト移動すると、状態がリセットされがちだ。`ng-template`や`TemplatePortal`ではビューが再生成され状態は失われる。しかし、Angular CDKの`DomPortal`を使えば、コンポーネント自体を再生成せずにDOMを移動させるため、状態を保持したまま移動できる。これにより、タイマーや「いいね」の状態も維持可能だ。
ITニュース解説
システムエンジニアを目指す皆さんにとって、Webアプリケーション開発は魅力的な分野でしょう。特に、Angularのようなフレームワークを使うと、複雑なUI(ユーザーインターフェース)も効率的に構築できます。しかし、時には思わぬ挙動に直面することもあります。今回は、Angularでコンポーネントを画面上の異なる場所に移動させた際に、そのコンポーネントが持っていたデータ(状態)が消えてしまう問題を解決する方法について解説する。
まず、どのような問題が起きるのか、具体的な例を見てみよう。とある管理ダッシュボードアプリには、サイドバーに「プロモバナー」が表示されている。このバナーには、ハートボタンとタイマーが付いている。ユーザーがボタンをクリックすると、このバナーがサイドバーからメインコンテンツ領域へ移動する。しかし、バナーが移動するたびに、タイマーはリセットされて最初からカウントし直し、ハートボタンの「いいね」も解除されてしまう。これは、コンポーネントが移動するたびに「再初期化」されているためである。
この問題の根本原因は、現在のコードにある。ルートコンポーネントのテンプレートでは、プロモバナーを表示する場所を切り替えるボタンがある。そして、dockRight()というシグナル(Angularの反応的な状態管理機能)の値に基づいて、@ifディレクティブを使って、プロモバナーをサイドバーかメインコンテンツ領域のどちらかに条件付きで表示している。
@if (!dockRight()) { <promo-banner></promo-banner> }
これは、dockRight()が真でなければメインコンテンツ領域にプロモバナーを表示し、
@if (dockRight()) { <promo-banner></promo-banner> }
これは、dockRight()が真であればサイドバーにプロモバナーを表示するという意味である。
この@ifディレクティブの仕組みがポイントだ。@ifは条件が切り替わるたびに、その中のコンポーネントを破棄し、新しいコンポーネントを再度作成する。つまり、コンポーネントのインスタンスが新しくなるため、それに紐づくタイマーや「いいね」の状態もリセットされてしまうのだ。この問題を解決するため、今回は3つの異なるアプローチを試していく。
1. ng-templateとngTemplateOutletを使った方法
まず最初に試すのは、Angularの組み込み機能であるng-templateとngTemplateOutletディレクティブを使う方法である。
ng-templateは、画面には直接表示されないが、他の場所で再利用できるHTMLのブロックを定義するものだ。ここでは、プロモバナーコンポーネントを含むng-templateを作成し、#promoという参照名を付ける。
<ng-template #promo><promo-banner></promo-banner></ng-template>
次に、この#promoテンプレートを、プロモバナーを表示したい場所でngTemplateOutletディレクティブを使って表示する。
@if (!dockRight()) { <ng-template [ngTemplateOutlet]="promo"></ng-template> }
@if (dockRight()) { <ng-template [ngTemplateOutlet]="promo"></ng-template> }
これにより、バナーは正しく両方の場所で表示される。しかし、残念ながら状態は以前と同じようにリセットされてしまう。その理由は、ngTemplateOutletがテンプレートを表示するたびに、新しい「組み込みビュー」を作成するからだ。新しいビューが作成されると、その中にあるコンポーネントも新しいインスタンスとして生成されるため、状態が失われるのである。
2. CDK TemplatePortalを使った方法
次に、Angular CDK (Component Dev Kit) のPortal Moduleを利用する方法を試す。CDKは、Angularアプリケーションで再利用可能なUIコンポーネントを構築するためのツール群を提供する。Portal ModuleにはTemplatePortal、ComponentPortal、DomPortalの3種類があるが、まずはTemplatePortalを試す。
TemplatePortalは、ng-templateと同じようにテンプレートを扱い、それを必要な場所に「アタッチ」(配置)できる仕組みである。
まず、npm install @angular/cdkでCDKをインストールし、PortalModuleをインポートする。
TypeScriptコードでは、promoContentというプロパティをTemplatePortal型で定義し、promoという名前でng-templateを参照するためのViewChild(テンプレート内の要素への参照を取得する機能)を設定する。さらに、テンプレートの「起源」となるViewContainerRef(新しいビューを作成するためのコンテナ)を注入する。
そして、コンストラクター内のeffect()(シグナルの変更に反応して処理を実行する機能)の中で、promoテンプレートが利用可能になったら、new TemplatePortal(this.promo(), this.viewContainerRef)を使ってpromoContentを初期化する。
テンプレート側では、ngTemplateOutletの代わりにcdkPortalOutletディレクティブを使う。
@if (!dockRight()) { <ng-template [cdkPortalOutlet]="promoContent"></ng-template> }
@if (dockRight()) { <ng-template [cdkPortalOutlet]="promoContent"></ng-template> }
これでバナーを移動させると、やはり状態はリセットされてしまう。TemplatePortalも、最終的には新しい組み込みビューを作成するため、コンポーネントのインスタンスが再生成されてしまうのだ。ComponentPortalを試しても結果は同じである。
3. CDK DomPortalを使った方法 (解決策)
いよいよ本命のDomPortalである。DomPortalの大きな特徴は、コンポーネントの「同じライブインスタンス」を実際に移動させることができる点だ。これは、既存のDOM(Document Object Model、Webページの構造)要素を、別のcdkPortalOutletの場所に「親を変更する」というイメージである。同じインスタンスが使われるため、当然、シグナルやタイマーの状態もそのまま維持される。
まず、TypeScriptコードを変更する。
promoContentプロパティの型をDomPortal<HTMLElement>に変更する。
promoビューチャイルドもTemplateRefからElementRef(DOM要素への直接的な参照)に変更する。DomPortalではViewContainerRefは不要なので削除できる。
effect()の中では、new DomPortal(this.promo())を使ってpromoContentを初期化する。
次に、テンプレートの変更である。
ng-templateの代わりに、実際のdiv要素でプロモバナーをラップし、それに#promoという参照名を付ける。
<div #promo><promo-banner></promo-banner></div>
そして、バナーを表示したい場所では、以前と同様にcdkPortalOutletディレクティブを使うが、今回はng-templateではなく、空のdiv要素にバインドする。
@if (!dockRight()) { <div [cdkPortalOutlet]="promoContent"></div> }
@if (dockRight()) { <div [cdkPortalOutlet]="promoContent"></div> }
これでアプリケーションを保存し、バナーを移動させてみよう。すると、タイマーは途切れることなくカウントを続け、ハートの「いいね」も解除されない!バナーは期待通り、状態を保持したまま移動した。
DomPortalは、コンポーネントのDOM要素を物理的に移動させるため、コンポーネントのインスタンスが再生成されることなく、その状態が完全に維持されるのである。また、コンポーネントが依存性注入(DI)によって受け取っていたサービスなどの文脈(DIコンテキスト)もそのまま保持されるというメリットもある。
まとめ
この体験から得られる最も重要なルールは次の通りである。
- コンポーネントを直接描画する、
ng-templateとngTemplateOutletを使う、あるいはTemplatePortalを使うといった方法では、ビューが毎回「再作成」され、コンポーネントのインスタンスも新しくなるため、状態がリセットされてしまう。 - しかし、
DomPortalを使えば、同じコンポーネントのインスタンスを実際に「移動」させることができるため、状態を完全に保持できる。
この知識は、Angularアプリケーションで複雑なUIの動きや状態管理を実装する上で非常に役立つだろう。Angular CDKのPortal Moduleは、今回紹介したDomPortalのように、アプリケーション開発を次のレベルに引き上げるための隠れた宝石のような機能が他にもたくさんある。ぜひ、色々な機能を試してみて、効率的でユーザーフレンドリーなアプリケーション開発を目指してほしい。