← 解説「コンポーネントが画面に生まれる手順 — 実行時の変化」に戻る
サンプルコードで身につける: コンポーネントが画面に生まれる手順 — 実行時の変化
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1誕生の瞬間は3つとも同じ手順
起動・条件表示・画面遷移は別々の機能に見えますが、内部では同じ誕生手順の呼び出し方が違うだけです。「同じ手順に合流する」と分かると、学んだ知識が1本につながります。
// 誕生手順(ホスト要素→DI→constructor→input→フック→作成モード)
// が呼ばれる3つの入り口:
//
// 1. アプリ起動
// bootstrapApplication(AppComponent)
// → index.html の <app-root> を host に、親なしで手順を実行
//
// 2. テンプレートの制御フロー
// @if (show()) { <app-dialog /> }
// → show() が true になった変更検知のタイミングで手順を実行
//
// 3. ルーティング
// { path: 'users', component: UserListComponent }
// → URL がマッチしたとき router-outlet の位置で手順を実行
//
// ViewContainerRef.createComponent() は、この手順を
// 自分のコードから直接起動するAPI(前レッスンの動的生成)
2コンストラクタでは input がまだ無い(実証)
誕生手順の(3)と(4)の順序を、ログで確かめられる形にした例です。「初期化はngOnInitで」という定石が、手順の順序から来る必然だと分かります。
import { Component, input, OnInit } from '@angular/core';
@Component({ selector: 'app-user-card', template: '...' })
export class UserCardComponent implements OnInit {
name = input(''); // 親は [name]="'松本'" を渡している
constructor() {
// 手順(3): input設定(4)より前なので、親の値はまだ無い
console.log(this.name()); // '' ← 初期値のまま!
}
ngOnInit() {
// 手順(5): input設定(4)の後なので、親の値が入っている
console.log(this.name()); // '松本'
}
}
// input() シグナルなら computed(() => this.name() + 'さん') の形で
// 「値が来たら追従する」定義をコンストラクタ時点で書けるため、
// この時差を意識する場面自体が減る(シグナル移行の御利益)
3inject() が使える場所・使えない場所の内部理由
「注入コンテキスト」という言葉を、誕生手順の時系列で説明し直します。エラーNG0203が出る場所と出ない場所の境界線が、手順(2)〜(3)のタイミングだと分かります。
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
@Component({ selector: 'app-profile', template: '...' })
export class ProfileComponent {
// ○ フィールド初期化子: constructor 実行の一部として動く。
// 手順(2)で自分のインジェクタが確定した直後なので特定できる
private userService = inject(UserService);
onClick() {
// × NG0203: ここは誕生手順が終わった後の「ただの関数呼び出し」。
// Angularは「どのコンポーネントの組み立て中か」をもう
// 知らないので、問い合わせ先インジェクタを特定できない
// const s = inject(UserService);
}
}
// 誕生手順の外で使いたい場合は、手順中に Injector 自体を
// 確保しておく: private injector = inject(Injector);
// → 後から runInInjectionContext(this.injector, () => ...)
4親子のライフサイクルログを予想する
誕生手順の(6)が再帰であることを、親子コンポーネントのログ順で確認します。「親のViewInitが最後」になる理由は、子の誕生が親のテンプレート実行の途中に挟まるからです。
// <app-parent> のテンプレートに <app-child> がある場合、
// 起動時のログはこの順になる:
//
// 親 constructor ← 親の手順(3)
// 親 ngOnInit ← 親の手順(5)。テンプレート実行より前!
// 子 constructor ← 親の手順(6)の途中で子の手順(3)が始まる
// 子 ngOnInit ← 子の手順(5)
// 子 ngAfterViewInit ← 子のテンプレートまで完成
// 親 ngAfterViewInit ← 子が全部済んで、やっと親のビューが完成
//
// 読み取れる法則:
// ・OnInit は「上から下」(親→子)
// ・AfterViewInit は「下から上」(子→親)
// ・親の ngOnInit 時点では子はまだ存在しない
// → viewChild の結果がここで取れない内部理由
5誕生は再帰 — @defer が効く内部理由
初回表示のコストが「配下ツリーの誕生手順の合計」であることを図で示します。@deferの境界線が、再帰のどこを切っているのかが見えると、どこに境界を引くべきかも判断できます。
// 商品ページの誕生コスト(誕生手順の実行回数)
//
// AppComponent
// └ ProductPage
// ├ ProductInfo(+子5個) … 誕生 6回
// ├ ReviewList(+行50個) … 誕生 51回 ← 重い!
// └ RelatedItems(+カード20個)… 誕生 21回
//
// 全部同期なら: 初回表示までに誕生 約80回ぶんの仕事
//
// @defer (on viewport) { <app-review-list /> } と境界を切ると:
// 初回: ProductInfo 側の誕生だけ(再帰がそこで止まる)
// 後で: 画面に入った時点で ReviewList 以下 51回ぶんを実行
//
// 「初回表示を軽くする」パフォーマンス編の話は、
// 内部的には「起動時に走る誕生再帰をどこで打ち切るか」の設計