← 解説「破棄と再利用 — コンポーネントが消えるときに起きること」に戻る
サンプルコードで身につける: 破棄と再利用 — コンポーネントが消えるときに起きること
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1破棄の逆再生を順番に追う
@ifが偽になった瞬間に走る破棄手順を、誕生手順と対にして並べます。誕生の逆順(子から先に)であること、後始末フックが最初に呼ばれることがポイントです。
// @if (show()) { <app-dialog /> } で show() が false になった瞬間:
//
// (1) 配下の子から順に、破棄フックを実行
// 子の ngOnDestroy → DestroyRef.onDestroy のコールバック
// → 親(Dialog自身)の ngOnDestroy → DestroyRef
// ※ takeUntilDestroyed の購読解除もここで発火
//
// (2) Angularが登録したイベントリスナーを解除
// (click)="..." などの作成モードで付けたもの
//
// (3) LView を親のビューツリーから切り離す
// → 以後、変更検知の巡回対象から外れる
//
// (4) ホスト要素ごとDOMから除去
//
// 誕生: 要素→DI→constructor→input→フック→テンプレート
// 破棄: フック→リスナー→ツリー→要素(おおよそ逆順)
2track item.id と track $index の破棄回数を数える
同じ並べ替え操作で、trackの書き方によって破棄+再誕生が何回走るかを数えます。@forのレッスンで学んだ「trackは同一性の宣言」の、内部コストとしての意味です。
// items: [A, B, C](id付きオブジェクト)を [C, B, A] に並べ替え
//
// ── track item.id ──
// C: id一致 → 既存LViewを移動して再利用
// B: id一致 → そのまま再利用
// A: id一致 → 既存LViewを移動して再利用
// 破棄0回・誕生0回(DOMの移動と更新モードだけ)
//
// ── track $index ──
// 位置0: 前回A ≠ 今回C → Aを破棄、Cを誕生
// 位置1: 前回B = 今回B → 再利用
// 位置2: 前回C ≠ 今回A → Cを破棄、Aを誕生
// 破棄2回・誕生2回(入力中のフォームなら入力値も消える!)
//
// 破棄+誕生は「フック実行→DOM除去→要素作成→DI→…」の
// フルコースなので、行数×頻度で効いてくる
3ルーターの再利用 — ngOnInit が2回目に呼ばれない理由
/users/1 から /users/2 への遷移でコンポーネントが破棄されない、というルーティング編の注意点を、破棄・再利用の内部動作として説明し直します。
// ルート定義: { path: 'users/:id', component: UserDetailComponent }
//
// /users/1 → /users/2 の遷移でルーターが考えること:
// 「同じルート設定・同じコンポーネント → 破棄はもったいない。
// インスタンスを再利用してパラメータだけ差し替えよう」
//
// その結果:
// ・破棄の逆再生は走らない(ngOnDestroy されない)
// ・誕生手順も走らない(constructor / ngOnInit されない)
// ・変わるのは ActivatedRoute の paramMap の値だけ
//
// だから ngOnInit で1回だけ id を読む書き方は2回目に壊れる:
// × const id = this.route.snapshot.paramMap.get('id');
// ○ 変化に追従する形で受け取る
// id = toSignal(this.route.paramMap.pipe(
// map((p) => p.get('id'))));
// user = httpResource(() => `/api/users/${this.id()}`);
4逆再生で消えないもの — リーク危険リスト
破棄手順が片付けるのは「Angularが台帳に記録したもの」だけです。台帳の外にあるものは自動では消えません。危険リストとDestroyRefへの相乗りをセットで覚えます。
// ── 破棄の逆再生が自動で消してくれるもの ──
// ・テンプレートの (click) などのリスナー
// ・子コンポーネント(再帰的に)
// ・asyncパイプ / toSignal / takeUntilDestroyed 済みの購読
//
// ── 台帳の外なので残り続けるもの(リーク源)──
// ・setInterval / setTimeout → clearが必要
// ・window.addEventListener('resize') → removeが必要
// ・外部ライブラリのインスタンス(地図・チャート) → dispose
// ・生の subscribe(解除演算子なし) → unsubscribe
//
// 対処はすべて同じ形: 発生させたその場で逆再生に相乗りする
// const id = setInterval(...);
// inject(DestroyRef).onDestroy(() => clearInterval(id));
//
// 「破棄されたはずの画面のログが出続ける」を見たら
// このリストの上から疑う
5誕生・更新・破棄 — コース全体をこの地図に載せる
内部機構セクションの総まとめです。コースで学んだ主要機能を「ビューの一生」のどこに効く話なのかで分類し直すと、独立に見えた知識が1枚の地図になります。
// ── ビューの一生と、コースで学んだことの対応 ──
//
// [ビルド時] AOTコンパイル・セレクタ照合・チャンク分割
// ↳ imports / 遅延ルート / バンドル分析
//
// [誕生] 要素→DI→constructor→input→フック→作成モード(再帰)
// ↳ inject() の場所制限 / ngOnInit の定石 / @defer /
// SSRはこれをサーバで実行、ハイドレーションで引き継ぎ
//
// [更新] 変更検知が dirty なビューの更新モードを実行
// ↳ シグナル(読む=依存登録) / OnPush / ゾーンレス /
// linkedSignal・resource もこの層の住人
//
// [破棄] 子から逆再生・リスナー解除・LView切り離し
// ↳ DestroyRef / takeUntilDestroyed / @for の track /
// ルーターの再利用
//
// 新しいAPIに出会ったら「どの段に効く道具か」を最初に
// 考えると、応用が効く(このセクションの目的)