はじめに
Angular技術要素解説シリーズ、第2弾です。
今回は、Angular 20で安定版となり、現在のAngular開発の中心的な考え方になっているSignals(シグナル)と、その背景にある「変更検知」の仕組みを解説します。
以前当シリーズで解説したリアクティブプログラミングの考え方が、Angularというフレームワークの中でどう活かされているかが分かる内容です。
この記事で扱う技術要素
- Change Detection(変更検知)
- Zone.js
- Signal / computed() / effect()
- Zoneless(ゾーンレス)
変更検知(Change Detection)とは
Webアプリは、「データが変化したら、画面(DOM)もそれに合わせて更新する」という仕組みが必要です。
この「データの変化を検知して、画面を更新する処理」全般を変更検知(Change Detection)と呼びます。
Angularは長年、Zone.jsというライブラリを使ってこの変更検知を実現してきました。
Zone.jsによる変更検知の仕組み(従来方式)
import { Component } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>カウント: {{ count }}</p>
<button (click)="increment()">+1</button>
`,
})
export class CounterComponent {
count = 0;
increment() {
this.count++; // ただのプロパティ代入。特別な通知処理は書いていない
}
}
このコードには、「画面を再描画してください」という指示は一切書かれていません。
それでもボタンをクリックすると画面のcount表示は正しく更新されます。
これは、Zone.jsがクリックイベント・タイマー・HTTP通信といった非同期処理をすべて監視しており、それらが発生するたびに「アプリ全体の変更検知」を自動的に走らせているためです。
この仕組みは非常に便利な反面、次のような課題も抱えていました。
- アプリ全体を毎回チェックするため、大規模なアプリではパフォーマンスに影響が出ることがある
- 「本当に画面を変える必要があったのは1箇所だけなのに、アプリ全体がチェックされる」という無駄が発生しやすい
- Zone.jsというライブラリ自体が、アプリのバンドルサイズや初期化コストに影響する
Signal(シグナル)とは
こうした課題を解決するために導入されたのがSignalです。
Signalは、「値の変化を追跡できるデータの入れ物」で、その値に依存している部分だけを、ピンポイントで効率よく再描画できる仕組みを提供します。
Signalの基本
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>カウント: {{ count() }}</p>
<button (click)="increment()">+1</button>
<button (click)="reset()">リセット</button>
`,
})
export class CounterComponent {
// signal()で「リアクティブな値」を作る
count = signal(0);
increment() {
// update()は「現在の値」を受け取り、「新しい値」を返す関数を渡す
this.count.update(current => current + 1);
}
reset() {
// set()は新しい値を直接指定する
this.count.set(0);
}
}
実行の流れ:
初期表示: カウント: 0
「+1」を3回クリック: カウント: 3
「リセット」をクリック: カウント: 0
ポイントは、テンプレート側でcountではなくcount()と関数のように呼び出している点です。
Signalは値そのものではなく「値を取得するための関数」であり、Angularはこの呼び出しを検知することで「このテンプレートはcountという値に依存している」と自動的に把握します。
値が変化したときは、count()を使っている部分だけがピンポイントで再描画されます。
computed():他のSignalから値を導出する
既存のSignalの値をもとに、別の値を自動計算したい場合はcomputed()を使います。
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-cart',
standalone: true,
template: `
<p>単価: {{ price() }}円</p>
<p>数量: {{ quantity() }}個</p>
<p>合計: {{ total() }}円</p>
<button (click)="quantity.update(q => q + 1)">数量+1</button>
`,
})
export class CartComponent {
price = signal(500);
quantity = signal(1);
// priceかquantityのどちらかが変化すると、totalも自動的に再計算される
total = computed(() => this.price() * this.quantity());
}
実行結果:
初期表示: 単価: 500円 / 数量: 1個 / 合計: 500円
「数量+1」を2回クリック後: 単価: 500円 / 数量: 3個 / 合計: 1500円
totalは、priceやquantityのように直接値を書き換えることはできません(読み取り専用)。
あくまで依存しているSignalの値から自動的に導き出される値という位置づけです。
quantityを更新するだけで、totalの再計算・再描画まで自動的に連動する点が、Signalの大きな利点です。
effect():値の変化に応じて副作用を実行する
Signalの値が変化するたびに、ログ出力やDOM操作、localStorageへの保存といった副作用を実行したい場合はeffect()を使います。
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-theme-switcher',
standalone: true,
template: `
<button (click)="toggleTheme()">テーマ切替(現在: {{ theme() }})</button>
`,
})
export class ThemeSwitcherComponent {
theme = signal<'light' | 'dark'>('light');
constructor() {
// themeが変化するたびに自動的に実行される
effect(() => {
console.log(`テーマが「${this.theme()}」に変更されました`);
localStorage.setItem('theme', this.theme());
});
}
toggleTheme() {
this.theme.update(current => (current === 'light' ? 'dark' : 'light'));
}
}
実行結果(コンソール):
テーマが「light」に変更されました ← 初回実行時にも1度呼ばれる
テーマが「dark」に変更されました ← ボタンクリック後
effect()は、画面の更新とは直接関係のない「副作用」を扱うためのものです。
画面に表示する値を計算したいだけならcomputed()を、ログ出力やストレージへの保存など画面外への影響を起こしたいならeffect()を、というように使い分けます。
Zoneless(ゾーンレス)への移行
Signalsが安定版になったことで、AngularはZone.jsに依存しない変更検知(Zoneless)への移行を進めています。
Angular 20.2でZonelessが安定版となりました。
// main.ts (Zonelessを有効にする設定例)
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';
import { AppComponent } from './app.component';
bootstrapApplication(AppComponent, {
providers: [
provideZonelessChangeDetection(), // Zone.jsを使わない変更検知を有効化
],
});
Zonelessモードでは、Angularは「Signalの値が変化した」というピンポイントな情報だけを頼りに再描画を行うため、Zone.jsによる「アプリ全体を毎回チェックする」オーバーヘッドがなくなります。
バンドルサイズの削減、実行時パフォーマンスの向上、そしてデバッグのしやすさの向上が期待されており、今後のAngularアプリはSignalsを前提とした設計が主流になっていく見込みです。
変更検知方式の比較
| 方式 | 検知のトリガー | 再描画の範囲 | 現在の位置づけ |
|---|---|---|---|
| Zone.js(従来方式) | 非同期処理(イベント・タイマー・HTTP等)全般を自動監視 | アプリ全体をチェック | 既存プロジェクトで広く使われている |
| Signals + Zoneless | Signalの値の変化をピンポイントで検知 | 依存している部分だけ | Angular 20.2以降の推奨方式、今後の主流 |
使用場面・使い分けの指針
- 新規にコンポーネントの状態管理をする場合は、まず
signal()を検討する - 複数のSignalから導出される値は
computed()にまとめ、テンプレート側で毎回計算しない - ログ出力・外部ストレージへの保存など、画面の値そのものではない処理は
effect()に書く - HTTP通信のようなストリーム処理は、次回以降の記事で解説するRxJSの役割が引き続き大きい(Signalsと完全に置き換わるものではない)
まとめ
| 用語 | 役割 |
|---|---|
| Change Detection | データの変化を画面に反映する仕組み全般 |
| Zone.js | 従来の変更検知を支えてきた、非同期処理監視ライブラリ |
| Signal | 値の変化を追跡できる、リアクティブなデータの入れ物 |
| computed() | 他のSignalから自動的に値を導出する、読み取り専用のSignal |
| effect() | Signalの変化に応じて副作用(ログ出力等)を実行する仕組み |
| Zoneless | Zone.jsに依存しない、Signalベースの変更検知方式(Angular 20.2で安定版) |
次回は、Signal導入後も引き続き重要な役割を持つRxJSとHTTP通信について解説します。
学習に使った書籍
本記事の内容整理にあたっても、以下の書籍を参考にしました。
- Angular 超入門 第2版 [ 掌田津耶乃 ]
- Angularアプリケーションプログラミング 開発の勘所と手順をしっかりマスター [ 山田祥寛 ]
- Angular Webアプリ開発スタートブック [ 大澤文孝 ]
※上記リンクはアフィリエイトリンクを含みます。