【Angular基礎講座】Signals徹底解説!リアクティブな状態管理の新しい波に乗ろう
※ 当ページには【広告/PR】を含む場合があります。
2025/07/13

「zoneless」
AngularでSignalsを使うメリットについて
Signalsの概要と有用性
* **きめ細やかな変更検知**:
Signalsは、値の変更を非常に細かく追跡します。
これにより、アプリケーション全体で不要な再レンダリングや再計算を防ぎ、パフォーマンスを向上させることができます。
* **リアクティブなプログラミングの簡素化**:
RxJSのような複雑なストリーム操作を必要とせず、
より直感的かつ宣言的な方法でリアクティブな状態管理を記述できます。
* **Zone.jsからの脱却**:
将来的な「zoneless」アプリケーションへの移行を見据えた基盤となります。
Zone.jsのオーバーヘッドを削減し、より高速なアプリケーションの実現に貢献します。
* **明確な依存関係**:
シグナル間の依存関係が明確になり、コードの可読性と保守性が向上します。
Signalsのコア機能
1. **Writable Signals (`signal`関数)**:
値を直接更新できるシグナルです。
初期値を`signal`関数に渡して作成し、
`.set()`メソッドで直接値を設定したり、
`.update()`メソッドで以前の値から新しい値を計算して設定したりできます。
2. **Computed Signals (`computed`関数)**:
他のシグナルから値を派生させる「読み取り専用」のシグナルです。
`computed`関数と、値を派生させるロジックを指定して定義します。
遅延評価され、メモ化されるため、計算コストの高い派生処理も効率的に実行できます。
3. **Effects (`effect`関数)**:
1つ以上のシグナル値が変更されるたびに実行される操作です。
常に少なくとも一度は実行され、
その中で読み取られたシグナル値を追跡し、
それらの値が変更されるたびに再度実行されます。
主に、表示されているデータのログ記録や`window.localStorage`とのデータ同期など、
副作用を伴う処理に利用されます。
Signalsの基本構文・主要なメソッドの使い方
`signal`関数:書き込み可能シグナル
signal
import { signal } from '@angular/core';
//初期値0でシグナルを作成します
const count = signal(0);
//シグナルの値を読み取るには、ゲッター関数として呼び出します
//👇出力 ... 現在のカウントは: 0
console.log('現在のカウントは: ' + count());
//set()メソッドで値を直接変更
count.set(3);
//👇出力 ... カウントは3になりました: 3
console.log('カウントは3になりました: ' + count());
//update()メソッドで以前の値から新しい値を計算して変更
count.update(value => value + 1);
//👇出力 ... 最終的なカウントは: 4
console.log('最終的なカウントは: ' + count());
`computed`関数:算出シグナル
computed
import { signal, computed } from '@angular/core';
const count = signal(0);
//countシグナルに依存する算出シグナルを作成
const doubleCount = computed(() => count() * 2);
//👇出力 ... 0 (初めて読み込まれたときに計算される)
console.log(doubleCount());
count.set(5);
//👇出力 ... 10 (countが変更されたため再計算)
console.log(doubleCount());
const showCount = signal(false);
const conditionalCount = computed(() => {
if (showCount()) {
return `カウントは ${count()} です。`;
} else {
return '何も表示されていません!';
}
});
//👇出力 ... '何も表示されていません!'
//※(showCountがfalseのためcountは読み取られない)
console.log(conditionalCount());
//conditionalCountは再計算されない
count.set(10);
//👇出力 ... '何も表示されていません!'
console.log(conditionalCount());
showCount.set(true);
//👇出力 ... 'カウントは 10 です。'
//※(showCountがtrueになり、countが読み取られる)
console.log(conditionalCount());
//conditionalCountは再計算されるようになる
count.set(15);
//👇出力 ... 'カウントは 15 です。'
console.log(conditionalCount());
`effect`関数:副作用の実行
effect
import {
signal, effect, untracked,
Injector, inject
} from '@angular/core';
import { Component } from '@angular/core';
@Component({
selector: 'app-counter',
template: ``,
standalone: true
})
export class CounterComponent {
readonly count = signal(0);
private readonly user = signal('Alice');
//コンストラクター内でエフェクトを作成するのが一般的
constructor() {
effect(() => {
console.log(`現在のカウントは: ${this.count()}`);
});
//依存関係を追跡せずにシグナルを読み込む例:
//userシグナルが変更されたときにのみエフェクトが実行される
effect(() => {
console.log(`ユーザーは ${this.user()} に設定され、カウントは ${untracked(this.count)} です`);
});
//クリーンアップ関数を持つエフェクトの例:
//エフェクトが再実行されるか破棄される前に、タイマーなどのリソースをクリア
effect((onCleanup) => {
const currentUserValue = this.user();
const timer = setTimeout(() => {
console.log(`1秒前、ユーザーは ${currentUserValue} になりました`);
}, 1000);
onCleanup(() => {
//エフェクトが再実行されるか破棄される前にタイマーをクリア
clearTimeout(timer);
});
});
}
//インジェクターを使用してコンストラクター外でエフェクトを作成する例
private injector = inject(Injector);
initializeLogging(): void {
effect(() => {
console.log(`カウントのログが初期化されました: ${this.count()}`);
}, { injector: this.injector });
}
increment() {
this.count.update(value => value + 1);
}
changeUser(newUser: string) {
this.user.set(newUser);
}
}
//シグナルの変更をトリガーしてエフェクトの動作を確認
const componentInstance = new CounterComponent();
//エフェクトが実行
componentInstance.increment();
//ユーザー変更エフェクトとクリーンアップエフェクトが実行
componentInstance.changeUser('Bob');
応用的な実装例
Signalの実装の基本
signal
computed
effect
Signalの応用
linkedSignal
resource
`linkedSignal`:他の状態に連動するシグナル
linkedSignal
signal
linkedSignal
import { signal, linkedSignal } from '@angular/core';
interface ShippingMethod {
id: number;
name: string;
}
const shippingOptions = signal<ShippingMethod[]>([
{ id: 0, name: 'Ground' },
{ id: 1, name: 'Air' },
{ id: 2, name: 'Sea' },
]);
//基本的なlinkedSignalの例 (常に最初のオプションにリンク)
const selectedOptionBasic = linkedSignal(() => shippingOptions());
//👇出力 ... { id: 0, name: 'Ground' }
console.log(selectedOptionBasic());
shippingOptions.set([
{ id: 0, name: 'Email' },
{ id: 1, name: 'Will Call' },
]);
//👇出力 ... { id: 0, name: 'Email' }
console.log(selectedOptionBasic());
//以前の状態を考慮するlinkedSignalの例:
//新しいオプションリストに以前選択されていたオプションがあればそれを維持し、
// なければ、リストの最初のオプションをデフォルトを返す
const selectedOptionAdvanced = linkedSignal<ShippingMethod[], ShippingMethod>({
source: shippingOptions,
computation: (newOptions, previous) => {
return (
newOptions.find(opt => opt.id === previous?.value.id) ?? newOptions[0]
);
},
});
//テスト
shippingOptions.set([
{ id: 0, name: 'Ground' },
{ id: 1, name: 'Air' },
{ id: 2, name: 'Sea' },
]);
//'Sea' を選択
selectedOptionAdvanced.set(shippingOptions()[2]);
//👇出力 ... { id: 2, name: 'Sea' }
console.log("初期選択:", selectedOptionAdvanced());
//配送オプションを変更します
shippingOptions.set([
{ id: 0, name: 'Email' },
{ id: 1, name: 'Sea' }, //'Sea'は新しいリストにも存在
{ id: 2, name: 'Postal Service' },
]);
//出力 ... { id: 1, name: 'Sea' } (以前の選択が維持される)
console.log("オプション変更後 (一致あり):", selectedOptionAdvanced());
//配送オプションをさらに変更 ('Sea'が存在しないリスト)
shippingOptions.set([
{ id: 0, name: 'Standard' },
{ id: 1, name: 'Express' },
]);
//出力 ... { id: 0, name: 'Standard' } (最初のオプションにデフォルト設定される)
console.log("オプション変更後 (一致なし):", selectedOptionAdvanced());
まとめ
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー