angular-signals
Angular v20以上において、シグナルベースのリアクティブな状態管理を実装します。signal()で反応性のある状態を作成し、computed()で派生状態を生成し、linkedSignal()で依存する状態を管理し、effect()で副作用を処理できます。状態管理に関する質問や、BehaviorSubject/Observableパターンからシグナルへの変更、またはリアクティブなデータフローの実装時に活用されます。
description の原文を見る
Implement signal-based reactive state management in Angular v20+. Use for creating reactive state with signal(), derived state with computed(), dependent state with linkedSignal(), and side effects with effect(). Triggers on state management questions, converting from BehaviorSubject/Observable patterns to signals, or implementing reactive data flows.
SKILL.md 本文
Angular Signals
シグナルは、状態管理のための Angular のリアクティブプリミティブです。同期的できめ細かなリアクティビティを提供します。
コアシグナル API
signal() - 書き込み可能な状態
import { signal } from '@angular/core';
// 書き込み可能なシグナルを作成
const count = signal(0);
// 値を読み取る
console.log(count()); // 0
// 新しい値を設定
count.set(5);
// 現在の値に基づいて更新
count.update(c => c + 1);
// 明示的な型指定
const user = signal<User | null>(null);
user.set({ id: 1, name: 'Alice' });
computed() - 派生状態
import { signal, computed } from '@angular/core';
const firstName = signal('John');
const lastName = signal('Doe');
// 派生シグナル - 依存関係が変わると自動的に更新
const fullName = computed(() => `${firstName()} ${lastName()}`);
console.log(fullName()); // "John Doe"
firstName.set('Jane');
console.log(fullName()); // "Jane Doe"
// 複雑なロジックを含む computed
const items = signal<Item[]>([]);
const filter = signal('');
const filteredItems = computed(() => {
const query = filter().toLowerCase();
return items().filter(item => item.name.toLowerCase().includes(query));
});
const totalPrice = computed(() => filteredItems().reduce((sum, item) => sum + item.price, 0));
linkedSignal() - リセット機能付き依存状態
import { signal, linkedSignal } from '@angular/core';
const options = signal(['A', 'B', 'C']);
// オプションが変わると最初のオプションにリセット
const selected = linkedSignal(() => options()[0]);
console.log(selected()); // "A"
selected.set('B'); // ユーザーが B を選択
console.log(selected()); // "B"
options.set(['X', 'Y']); // オプション変更
console.log(selected()); // "X" - 最初に自動リセット
// 前の値にアクセス
const items = signal<Item[]>([]);
const selectedItem = linkedSignal<Item[], Item | null>({
source: () => items(),
computation: (newItems, previous) => {
// 選択がまだ存在する場合は保持を試みる
const prevItem = previous?.value;
if (prevItem && newItems.some(i => i.id === prevItem.id)) {
return prevItem;
}
return newItems[0] ?? null;
},
});
effect() - 副作用
import { signal, effect, inject, DestroyRef } from '@angular/core';
@Component({...})
export class Search {
query = signal('');
constructor() {
// クエリが変わると effect が実行
effect(() => {
console.log('Search query:', this.query());
});
// クリーンアップ付き effect
effect((onCleanup) => {
const timer = setInterval(() => {
console.log('Current query:', this.query());
}, 1000);
onCleanup(() => clearInterval(timer));
});
}
}
Effect のルール:
- インジェクションコンテキスト内で実行(コンストラクタまたは
runInInjectionContextで) - コンポーネント破棄時に自動的にクリーンアップされます
コンポーネント状態パターン
@Component({
selector: 'app-todo-list',
template: `
<input [value]="newTodo()" (input)="newTodo.set($any($event.target).value)" />
<button (click)="addTodo()" [disabled]="!canAdd()">追加</button>
<ul>
@for (todo of filteredTodos(); track todo.id) {
<li [class.done]="todo.done">
{{ todo.text }}
<button (click)="toggleTodo(todo.id)">切り替え</button>
</li>
}
</ul>
<p>{{ remaining() }} 件残り</p>
`,
})
export class TodoList {
// 状態
todos = signal<Todo[]>([]);
newTodo = signal('');
filter = signal<'all' | 'active' | 'done'>('all');
// 派生状態
canAdd = computed(() => this.newTodo().trim().length > 0);
filteredTodos = computed(() => {
const todos = this.todos();
switch (this.filter()) {
case 'active':
return todos.filter(t => !t.done);
case 'done':
return todos.filter(t => t.done);
default:
return todos;
}
});
remaining = computed(() => this.todos().filter(t => !t.done).length);
// アクション
addTodo() {
const text = this.newTodo().trim();
if (text) {
this.todos.update(todos => [...todos, { id: crypto.randomUUID(), text, done: false }]);
this.newTodo.set('');
}
}
toggleTodo(id: string) {
this.todos.update(todos => todos.map(t => (t.id === id ? { ...t, done: !t.done } : t)));
}
}
RxJS 相互運用
toSignal() - Observable からシグナルへ
import { toSignal } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({...})
export class Timer {
private http = inject(HttpClient);
// Observable から - 初期値または allowUndefined が必要
counter = toSignal(interval(1000), { initialValue: 0 });
// HTTP から - ロード完了まで undefined
users = toSignal(this.http.get<User[]>('/api/users'));
// 同期 Observable (BehaviorSubject) の場合 requireSync を使用
private user$ = new BehaviorSubject<User | null>(null);
currentUser = toSignal(this.user$, { requireSync: true });
}
toObservable() - シグナルから Observable へ
import { toObservable } from '@angular/core/rxjs-interop';
import { switchMap, debounceTime } from 'rxjs';
@Component({...})
export class Search {
query = signal('');
private http = inject(HttpClient);
// RxJS 演算子を使用するためにシグナルを Observable に変換
results = toSignal(
toObservable(this.query).pipe(
debounceTime(300),
switchMap(q => this.http.get<Result[]>(`/api/search?q=${q}`))
),
{ initialValue: [] }
);
}
シグナル等価性
// カスタム等価性関数
const user = signal<User>({ id: 1, name: 'Alice' }, { equal: (a, b) => a.id === b.id });
// ID が変わった場合のみ更新をトリガー
user.set({ id: 1, name: 'Alice Updated' }); // 更新なし
user.set({ id: 2, name: 'Bob' }); // 更新をトリガー
未追跡の読み取り
import { untracked } from '@angular/core';
const a = signal(1);
const b = signal(2);
// 'a' のみに依存し、'b' には依存しない
const result = computed(() => {
const aVal = a();
const bVal = untracked(() => b());
return aVal + bVal;
});
サービス状態パターン
@Injectable({ providedIn: 'root' })
export class Auth {
// プライベートな書き込み可能状態
private _user = signal<User | null>(null);
private _loading = signal(false);
// パブリックな読み取り専用シグナル
readonly user = this._user.asReadonly();
readonly loading = this._loading.asReadonly();
readonly isAuthenticated = computed(() => this._user() !== null);
private http = inject(HttpClient);
async login(credentials: Credentials): Promise<void> {
this._loading.set(true);
try {
const user = await firstValueFrom(this.http.post<User>('/api/login', credentials));
this._user.set(user);
} finally {
this._loading.set(false);
}
}
logout(): void {
this._user.set(null);
}
}
高度なパターン (resource() を含む) については、references/signal-patterns.md を参照してください。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- plastikaweb
- ライセンス
- MIT
- 最終更新
- 2026/5/10
Source: https://github.com/plastikaweb/plastikspace / ライセンス: MIT