0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Angular技術要素解説】Signalsとは?リアクティブな状態管理と変更検知の仕組みを理解する

0
Posted at

はじめに

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通信について解説します。

学習に使った書籍

本記事の内容整理にあたっても、以下の書籍を参考にしました。

※上記リンクはアフィリエイトリンクを含みます。

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?