Angular 22のコンポーネント設計をレビュー視点で整理する100観点
Angular 22 は、変更検知の既定を OnPush に変え、フォームを
form()と[formField]に移し、テンプレートに@boundaryを加えた。NgModule とデコレーター入力を前提にした観点リストは、問いの前提から成り立たなくなっている。ここでは 22.2 系を前提に、コンポーネントとその周辺のレビュー観点を100項目へ整理する。
対象は @angular/core 22.2.1 と、同じ系列の @angular/common / forms / router / aria である。状態の持ち方そのもの(signal の設計、NgRx SignalStore、NgRx Store、RxJS)は本リストの範囲に入れていない。状態管理は API ごとに契約と誤解の仕方が違うため、別のリストに分けてある。個別論点の解説はng状態管理にある。
各群は「群の問い」から始まる。差分を読みながらその問いに yes か no で答えられるかを確かめ、答えられない項目をレビューコメントへ変換する、という順で使う想定である。
22 で前提が変わり、旧来の指摘がそのままでは通らなくなった箇所を先に挙げる。
| 箇所 | 21 までの前提 | 22.2 の前提 |
|---|---|---|
| 変更検知 | changeDetection 省略時は毎回チェック |
省略時は OnPush。毎回チェックは Eager(Default は非推奨) |
| 入出力 | @Input / @Output デコレーター |
input() / output() / model() |
| 制御フロー | *ngIf / *ngFor |
@if / @for / @switch / @defer / @boundary |
| 安全なナビゲーション | a?.b は null を返す |
undefined を返す |
| フォーム | FormGroup と [formControl] |
form() と [formField] |
| ハイドレーション | withIncrementalHydration() で有効化 |
既定で有効(同関数は非推奨) |
canMatch |
引数は route と segments |
第3引数 currentSnapshot が必須 |
✅ 入出力契約(01〜10)
親が何を渡し、子が何を返すかを、型と signal で宣言できているかを問う。
input() を @Input の書き方違いと見ると、この群の半分は指摘できなくなる。両者の差は構文ではなく、読み取りが依存として記録されるかどうかにある。
09 の根拠は依存の記録位置にある。input() はシグナルであり、テンプレートでの読み取りがそのビューの依存として残る。この依存があるため OnPush のビューでも親の更新が届く。入力値を ngOnInit で別の signal へ写すと依存が切れ、親が新しい値を渡しても表示が変わらなくなる。
✅ テンプレート制御フロー(11〜20)
制御フロー構文が、描画されるノードの寿命と再生成の条件を正しく表しているかを問う。
@for の track は、書いてあれば満たされる項目ではない。track は DOM ノードの同一性の判定式であり、誤った式を置くと、入力中の値や focus が別の行へ移る。
20 は 22.0 の破壊的変更に対応する。a?.b が返す値が null から undefined に変わったため、a?.b === null は常に false になる。型が null を含んでいれば比較自体はコンパイルを通り、条件が成立しない分岐として残る。
17 の背景は分割の仕組みにある。@defer はブロック内の依存を別チャンクへ分ける。on viewport を、スクロールしても視野に入らない位置に置けば、そのチャンクは読み込まれないまま残る。
✅ 変更検知と描画(21〜30)
このコンポーネントが再チェックされる条件を、差分だけから言えるかを問う。
既定が OnPush になったことで、変更検知の指摘が要らなくなったわけではない。既存プロジェクトの ng update は、明示指定のないコンポーネントへ Eager を書き込んで挙動を保つ。付け忘れを探す作業が、外し忘れを探す作業に置き換わった。
@Component({
selector: 'app-user-list',
changeDetection: ChangeDetectionStrategy.Eager, // 移行時に自動で入る。残ったままになりやすい
template: `...`,
})
export class UserListComponent {}23 と 30 が指すのは、同じ条件の裏表である。OnPush のビューが dirty になるのは、入力の参照が変わったとき、テンプレートで読んだ signal が変わったとき、自ビュー内でイベントが起きたとき、markForCheck() が呼ばれたときだけである。配列を破壊的に書き換える更新はこのどれにも当たらないため、Eager を外した時点で表示が止まる。逆に、毎回新しい配列を返すメソッドは参照が常に変わるので、チェックのたびに再評価される。
✅ クエリとホスト(31〜40)
テンプレート内の要素と宿主要素への参照が、存在しうるタイミングと型で取れているかを問う。
viewChild() を @ViewChild と同じものと見て、ngAfterViewInit 以降なら必ず取れると考えると、@if や @defer の内側で undefined が返る経路を見落とす。
34 と 40 の根拠は解決のタイミングにある。signal クエリは読み取りのたびにビューの現状から解決される。結果を ngOnInit で変数へ写すと、@if の切り替えで差し替わった要素を追えなくなり、破棄済みの要素への参照が残る。
✅ DIとサービス境界(41〜50)
依存の提供位置と注入タイミングが、インスタンスの寿命と一致しているかを問う。
22 では @Service() が @Injectable({ providedIn: 'root' }) の短縮形として加わった。同じ意味の書き方が2つある状態は、書き方の差を意図の差と読み違えさせる。
44 をレビューで拾う理由は、他に拾う場所がないからである。inject() が使えるのは注入コンテキストの中だけで、非同期処理の後やイベントハンドラの中で呼ぶと実行時に失敗する。この呼び出しは型の上では正しく、コンパイルを通る。
✅ ルーティング(51〜60)
ルート定義が、遅延境界、入力の受け渡し、データ取得の責務を分けているかを問う。
canMatch の形は 22.0 で変わった。第3引数 currentSnapshot が必須になったため、2引数のまま書かれたガードは型エラーになる。移行の途中で any を挟んだ箇所が残っていれば、型チェックは通り抜ける。
56 が問題になるのは、router resources の前提を崩すからである。一致したすべてのルートの resource は並列に読み込まれる。親の解決を待って子が動く従来のウォーターフォールを避ける仕組みであり、ctx を通じて互いの値を待たせると、並列である意味が消える。
なお router resources と @boundary は、22.2 時点では developer preview である。レビューで採用を勧める場合は、この段階にあることを前提にする。
✅ Signal Forms(61〜70)
フォームの値の所在と、検証が走るタイミングが form() の契約どおりかを問う。
form() がモデルのコピーを持つと考えると、この群はほぼ指摘できない。form() に渡した WritableSignal は複製されず、フィールドへの書き込みは元のモデルに反映される。
61 が捕まえるのは、値と状態のずれである。form はモデルを唯一の値の所在として扱い、dirty と touched はフォーム側の操作で更新される。別経路でモデルを set すると、値だけが入れ替わって dirty が false のまま残り、未編集として扱われる。
62 は綴りの問題に見えて、綴りの問題ではない。ディレクティブ名は FormField、セレクタは [formField] である。旧系の [formControl] や、設計段階の名前だった [control] は解決されず、バインドのないただの属性として残る。
✅ アクセシビリティと@angular/aria(71〜80)
対話的な UI を自作しているか、@angular/aria の挙動契約に乗せているかを問う。
role を書けばアクセシブルになる、とは言えない。role は支援技術に対する約束であり、キーボード操作と focus 管理が伴わなければ約束を破った状態になる。22 で一般提供になった @angular/aria には、accordion / combobox / grid / listbox / menu / tabs / toolbar / tree とそれぞれの testing ハーネスが入っている。
72 の根拠は実装の前提にある。@angular/aria の各コンポーネントは、特定の role と DOM 構造を前提にキーボード処理を組み立てている。role を上書きすると、支援技術が通知する操作と実際に用意された処理がずれ、矢印キーが無反応になる。
✅ SSR・ハイドレーション・描画コスト(81〜90)
サーバーで描いた DOM と、クライアントが最初に描く DOM が一致するかを問う。
ハイドレーションを明示的に有効化するもの、と見ている設定は 22 では古い。インクリメンタルハイドレーションが既定になり、withIncrementalHydration() は非推奨である。
82 が速度の問題になる筋道は、再利用の失敗を通る。ハイドレーションはサーバー出力の DOM をそのまま使い回す前提で動く。クライアントの初回描画が別のノードを作ると、一致しない領域が破棄されて作り直され、サーバーで描いた分の利点が消える。Date.now() を描画に混ぜたコンポーネントがこれに当たる。
✅ テストと検証(91〜100)
このコンポーネントの契約が、テストで固定されているかを問う。
fixture.detectChanges() を呼べば描画が反映される、という前提は OnPush 既定と zoneless 構成では崩れる場面がある。
91 が落とし穴になるのは、detectChanges() が dirty なビューしか再評価しないからである。ComponentFixture.detectChanges() は fixture のビューをチェックするが、OnPush のコンポーネントは dirty でなければ中身が評価されない。入力を signal 経由で更新していないテストは、値を変えたのに古い DOM を検証したまま通る。
signal と computed の設計、Resource の status、SignalStore、NgRx Store、RxJS の購読の寿命は、ここに含めていない。4系統はそれぞれ別の API 契約を持ち、1本に押し込めば1系統あたり十数項目にしかならないためである。個別論点の解説はng状態管理にある。NgModule 構成を前提にした旧版はAngularコードレビュー観点100選として残してある。