Dev Study
← 解説「ゾーンレスとOnPushの既定化」に戻る

サンプルコードで身につける: ゾーンレスとOnPushの既定化

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

1シグナル更新で画面が変わる最小例

ゾーンレスのAngularでは、画面更新のきっかけは「シグナルが変わった」という明示的な変更です。count.set() を呼べばAngularが変更を確実に検知し、その部分だけを再描画します。zone.js のように「何か起きたかもしれないから全体を見直す」仕組みには頼っていません。

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

@Component({
  selector: 'app-like-button',
  // シグナルの変更がそのまま画面更新のきっかけになる
  template:
    '<button (click)="likes.set(likes() + 1)">いいね {{ likes() }}</button>',
})
export class LikeButtonComponent {
  likes = signal(0);
}

// zone.js の頃: 「何か起きたかも」のたびに画面全体をチェックしていた
// ゾーンレス: likes が変わったという明示的な通知だけで更新される

2provideZonelessChangeDetection でゾーンレスを指定する

v22で新規作成したアプリは最初から zone.js が外れていますが、それ以前から移行するアプリでは provideZonelessChangeDetection() をアプリの providers に追加してゾーンレスへ切り替えます。アプリ全体の変更検知の方式を決める、起動設定レベルの指定です。

import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';
import { AppComponent } from './app/app.component';

// アプリ起動時の providers でゾーンレスを指定する
// (v22で新規作成したアプリは、最初からゾーンレスが既定)
bootstrapApplication(AppComponent, {
  providers: [
    provideZonelessChangeDetection(),
  ],
});

// これで zone.js に頼らず、シグナルなどの明示的な変更だけが
// 画面更新のきっかけになる

3OnPush を明示的に指定する

v22では何も書かなくても OnPush が既定ですが、意図を明確にするために明示することもできます。OnPush のコンポーネントは「入力(input)が変わった」「シグナルが変わった」「イベントが発生した」ときだけ再描画されます。旧来の「常にチェックする」挙動が必要なときだけ、ChangeDetectionStrategy.Eager を指定します。

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

@Component({
  selector: 'app-user-badge',
  // v22 では既定なので書かなくても同じ(ここでは意図を明示)
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: '<span>{{ name() }}</span>',
})
export class UserBadgeComponent {
  // 入力が変わったときだけ再描画される
  name = input('ゲスト');
}

// 旧来の「常にチェック」挙動が要る場合だけ、明示的に指定する:
//   changeDetection: ChangeDetectionStrategy.Eager

4シグナルを使っていれば OnPush を意識しなくてよい

state をシグナルで持っている限り、OnPush だからといって特別なことは何もありません。set() や update() を呼ぶだけで、Angularが変わった場所を正確に把握して再描画します。これまでのレッスンで「状態はシグナルで持つ」と繰り返してきた方針が、v22ではそのまま最適な書き方になっている、というのがこの例のポイントです。

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

// OnPush が既定でも、シグナルで書いていれば普段どおりでよい
@Component({
  selector: 'app-cart-summary',
  template:
    '<p>カート: {{ items().length }} 点 / 合計 {{ total() }} 円</p>' +
    '<button (click)="addItem()">100円の商品を追加</button>',
})
export class CartSummaryComponent {
  items = signal<number[]>([]);
  total = computed(() => this.items().reduce((sum, p) => sum + p, 0));

  addItem() {
    // update() するだけ。OnPush を意識したコードはどこにもない
    this.items.update((list) => [...list, 100]);
  }
}

5setTimeout の中でもシグナルなら画面に反映される

OnPush で注意が必要なのは、普通のプロパティを setTimeout などの外から書き換えると画面が自動更新されないことがある点です。しかしシグナルなら、どこから set() を呼んでも変更が確実に画面へ反映されます。「タイマーや非同期処理で表示が更新されない」という古典的なハマりどころが、シグナルで消える例です。

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

@Component({
  selector: 'app-countdown',
  template: '<p>残り {{ seconds() }} 秒</p>',
})
export class CountdownComponent {
  seconds = signal(10);

  constructor() {
    // setTimeout の中からの更新でも、シグナルなら確実に画面へ反映される
    setTimeout(() => {
      this.seconds.set(9);
    }, 1000);

    // もし seconds が普通のプロパティだったら、OnPush では
    // この書き換えに画面が追従しないことがある
  }
}