サンプルコードで身につける: effect — シグナルの変更に反応する
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1シグナルの変化をログに出す
effect の最小例で、コンストラクタの中に登録した関数が count の変更のたびに再実行されます。どのシグナルを追跡するかを自分で指定していないのに、関数の中で読んでいる count が自動で追跡される点が核心です。まずは console.log で再実行のタイミングを観察するのが理解の近道です。
import { Component, effect, signal } from '@angular/core';
@Component({
selector: 'app-counter-logger',
template: '<button (click)="add()">+1</button>',
})
export class CounterLoggerComponent {
count = signal(0);
constructor() {
// 中で読んでいる count が自動で追跡される
effect(() => {
console.log('現在のカウント:', this.count());
});
}
add() {
this.count.update((v) => v + 1);
}
}
2effect の仕組みを自作して動かす
「登録した瞬間にまず1回実行し、依存が変わるたびに再実行する」という effect の動きを、プレーンなTypeScriptで再現した例です。実行すると、登録直後に1回、write のたびに1回ずつ関数が動くことを確認できます。この「最初に1回」は実際のAngularのeffectでも同じ挙動です。
3下書きを localStorage に自動保存する
「シグナルの値が変わったらブラウザのストレージに保存する」という、effect の代表的な実務ユースケースです。localStorage はAngularの外の世界なので、テンプレートではなく effect が橋渡し役になります。エディタの下書き自動保存や設定の永続化はこのパターンで実装します。
import { Component, effect, signal } from '@angular/core';
@Component({
selector: 'app-draft-saver',
template: '<input (input)="onInput($event)" placeholder="下書き">',
})
export class DraftSaverComponent {
draft = signal('');
constructor() {
// 画面の外(localStorage)への橋渡しはeffectの出番
effect(() => {
localStorage.setItem('draft', this.draft());
});
}
onInput(e: Event) {
this.draft.set((e.target as HTMLInputElement).value);
}
}
4未読件数をブラウザのタブに反映する
document.title はテンプレートの外にあるため、シグナルの変更を届けるには effect を使います。未読が増えるたびにタブの表示が「受信箱 (3)」のように更新される、メールやチャットアプリでおなじみの動きです。「テンプレートで描画できないものへの反映」という effect の守備範囲がよく分かる例です。
import { Component, effect, signal } from '@angular/core';
@Component({
selector: 'app-unread-title',
template: '<button (click)="receive()">新着メールを受信</button>',
})
export class UnreadTitleComponent {
unreadCount = signal(0);
constructor() {
// ブラウザのタブ表示というAngularの外の世界へ変更を届ける
effect(() => {
document.title = '受信箱 (' + this.unreadCount() + ')';
});
}
receive() {
this.unreadCount.update((v) => v + 1);
}
}
5検索の確定を分析基盤へ送る
「検索が確定するたびに分析イベントを送る」という、実務のログ収集を想定した例です。送信処理を effect に寄せることで、検索を実行するコードがどこに増えても記録漏れが起きません。初期値の空文字では送らない、というガード条件の置き方も実務でよく使う形です。
import { Component, effect, signal } from '@angular/core';
declare function sendAnalytics(eventName: string, data: object): void;
@Component({
selector: 'app-search-tracker',
template: '<button (click)="search()">りんごを検索</button>',
})
export class SearchTrackerComponent {
submitted = signal('');
constructor() {
effect(() => {
// 初期値の空文字では送らない、というガードを置く
if (this.submitted() !== '') {
sendAnalytics('search', { keyword: this.submitted() });
}
});
}
search() {
this.submitted.set('りんご'); // 実際は入力欄の値を渡す
}
}