Dev Study
← 解説「Angular DevTools — 部品の中身を覗く拡張機能」に戻る

サンプルコードで身につける: Angular DevTools — 部品の中身を覗く拡張機能

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1コンポーネントツリーで部品の有無を確かめる

「表示されない」の最初の確認は、そもそも部品が生まれているかです。Angular DevTools のツリーに部品が無ければ @if の条件を疑えます。値の調査より先にツリーを見る癖を付けましょう。

// バグ: 管理者バッジ(app-admin-badge)が表示されない

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-profile',
  template:
    '<h2>プロフィール</h2>' +
    '@if (role() === "admin") { <app-admin-badge /> }',
  imports: [AdminBadge],
})
export class Profile {
  // 原因: APIが返す値は 'Admin'(大文字始まり)なのに
  //       'admin' と比較しているため @if が常に false
  role = signal('Admin');
}

// Angular DevTools での調査:
//   Components ツリー:
//     app-profile        ← ある
//       (app-admin-badge が無い!)
//   app-profile を選ぶと右側に role: 'Admin' と見える
//   → 「値はAdmin、条件はadmin」の食い違いを目で確認できる
// 直し方: 比較を揃える(role().toLowerCase() === 'admin' 等)

2input の値を見て親子のどちらを疑うか決める

子の表示がおかしいとき、DevTools で子の input を見れば「親からの受け渡し」と「子の表示処理」のどちらが悪いか即決できます。この例では親のバインド忘れが input の undefined として現れます。

// バグ: 子コンポーネントの名前欄が空になる

import { Component, input } from '@angular/core';

@Component({
  selector: 'app-user-card',
  template: '<p>{{ user()?.name }}</p>',
})
export class UserCard {
  user = input<{ name: string }>();
}

// 親テンプレート側(バグはここ):
//   <app-user-card user="currentUser" />
//   ^^^^ 原因: [ ] を忘れたので 'currentUser' という文字列を
//        渡そうとして型が合わず、実際には何も渡っていない

// Angular DevTools での調査:
//   ツリーで app-user-card を選ぶ → 右側の表示:
//     user (input)  undefined
//   → 子に届いた時点で空 = 悪いのは親からの受け渡し側と確定
//   (もし user に正しい値が見えたら、疑うのは子のテンプレート)
//
// 直し方: [user]="currentUser" とプロパティバインディングにする

3「データは正しいのに表示がおかしい」を切り分ける

DevTools で signal の値が正しいと確認できれば、残る容疑者はテンプレートだけです。この例では値は正しく、パイプの指定ミスという表示側の原因にたどり着きます。

// バグ: 価格が「1980」と表示される(「1,980円」にしたい)

import { Component, signal } from '@angular/core';
import { DecimalPipe } from '@angular/common';

@Component({
  selector: 'app-price',
  // 原因: number パイプを書いたつもりが imports に入れ忘れて
  //       いたため、パイプなしのテンプレートに書き戻していた
  template: '<p>{{ price() }}円</p>',
  imports: [DecimalPipe],
})
export class Price {
  price = signal(1980);
}

// Angular DevTools での切り分け:
//   app-price を選ぶ → price (signal): 1980
//   → データは正しい。悪いのは100%表示側と確定
//   → テンプレートを見直す作業だけに集中できる
//
// 直し方: パイプを通す
//   template: '<p>{{ price() | number }}円</p>'
//   → 「1,980円」と表示される

4Profiler で重い描画の犯人を探す

入力のたびに画面がカクつくときは、Profiler で変更検知を録画します。棒グラフからどのコンポーネントに時間がかかったかが分かり、テンプレート内の重い関数呼び出しといった原因に絞り込めます。

// バグ: 検索欄に1文字打つたびに画面全体がカクつく

@Component({
  selector: 'app-report',
  // 原因: テンプレートから重い関数を直接呼んでいる
  //       変更検知のたびに毎回再計算されてしまう
  template: '<p>集計: {{ heavySummary() }}</p>',
})
export class Report {
  heavySummary(): number {
    let sum = 0;
    for (let i = 0; i < 10_000_000; i++) { sum += i % 7; }
    return sum;
  }
}

// Profiler での調査:
//   1. Angular DevTools → Profiler → 録画開始
//   2. 検索欄に数文字入力 → 録画停止
//   3. 棒グラフ: 1打鍵ごとに変更検知が1本。app-report が毎回 120ms
//   → 犯人は app-report の再計算と特定できる
//
// 直し方: computed に置き換えると依存が変わったときだけ再計算される
//   summary = computed(() => this.calcFrom(this.rows()));
//   template: '<p>集計: {{ summary() }}</p>'

5input を初回だけコピーするバグを見抜く

DevTools で「input は新しい値なのに表示が古い」と見えたら、子が input を別プロパティにコピーして使っている疑いがあります。input は都度読み出すか computed で派生させるのが正しい形です。

// バグ: 親でユーザーを切り替えても子の表示名が古いまま

import { Component, computed, input } from '@angular/core';

@Component({
  selector: 'app-greeting',
  template: '<p>{{ displayName }}</p>',
})
export class Greeting {
  name = input('');

  // 原因: 生成時に1回だけ input を読んでコピーしている
  //       その後 input が変わってもこの値は更新されない
  displayName = this.name() + ' さん';
}

// Angular DevTools での見え方:
//   name (input)   '田中'   ← 新しい値がちゃんと届いている
//   displayName    '佐藤 さん' ← 表示用の値だけ古い
//   → 「届いているのに反映されない」= コピーの断絶を疑う
//
// 直し方: computed で派生させれば input の変化に追従する
//   displayName = computed(() => this.name() + ' さん');
//   テンプレートは {{ displayName() }} にする