Dev Study
← 解説「破棄と再利用 — コンポーネントが消えるときに起きること」に戻る

サンプルコードで身につける: 破棄と再利用 — コンポーネントが消えるときに起きること

解説で学んだ概念を、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に出会ったら「どの段に効く道具か」を最初に
// 考えると、応用が効く(このセクションの目的)